diff --git a/.babelrc b/.babelrc deleted file mode 100644 index 9a451693..00000000 --- a/.babelrc +++ /dev/null @@ -1,8 +0,0 @@ -{ - "presets": [ - ["@babel/preset-env", { - "modules": "commonjs" - }] - ], - "plugins": [] -} diff --git a/.buckconfig b/.buckconfig new file mode 100644 index 00000000..53b35069 --- /dev/null +++ b/.buckconfig @@ -0,0 +1,23 @@ +[repositories] +root = . +prelude = prelude +toolchains = toolchains +none = none + +[repository_aliases] +config = prelude +ovr_config = prelude +fbcode = none +fbsource = none +fbcode_macros = none +buck = none + +[parser] +target_platform_detector_spec = target:root//...->prelude//platforms:default + +[project] +ignore = .git + +[llvm] +prefix = /opt/homebrew/opt/llvm +suffix = diff --git a/test/expected/cfa1.js.expected-out b/.buckroot similarity index 100% rename from test/expected/cfa1.js.expected-out rename to .buckroot diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index a12aae84..00000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,149 +0,0 @@ -version: 2.1 - -commands: - attach_ejs_workspace: - steps: - - attach_workspace: - at: /home/circleci - persist_ejs_workspace: - steps: - - persist_to_workspace: - root: /home/circleci - paths: - - project - install_native_deps: - steps: - - run: sh ci/setup-pkg-linux.sh - - run: sh ci/install-llvm-linux.sh - -jobs: - setup: - docker: - - image: cimg/node:20.6.1 - environment: - LLVM_SUFFIX: "" - steps: - - checkout - - install_native_deps - - run: sudo npm install -g node-gyp - - run: npm ci - - run: make all-recurse - - persist_ejs_workspace - - package: - docker: - - image: cimg/node:20.6.1 - steps: - - attach_ejs_workspace - - run: echo hi - - build-stage0: - docker: - - image: cimg/node:20.6.1 - steps: - - attach_ejs_workspace - - install_native_deps - - run: make stage0 - - check-stage0: - docker: - - image: cimg/node:20.6.1 - steps: - - attach_ejs_workspace - - install_native_deps - - run: make check-stage0 - - build-stage1: - docker: - - image: cimg/node:20.6.1 - steps: - - attach_ejs_workspace - - install_native_deps - - run: make stage1 - - persist_to_workspace: - root: . - paths: - - ejs.exe.stage1 - - check-stage1: - docker: - - image: cimg/node:20.6.1 - steps: - - attach_ejs_workspace - - install_native_deps - - run: make check-stage1 - - build-stage2: - docker: - - image: cimg/node:20.6.1 - steps: - - attach_ejs_workspace - - install_native_deps - - run: make stage2 - - persist_to_workspace: - root: . - paths: - - ejs.exe.stage2 - - check-stage2: - docker: - - image: cimg/node:20.6.1 - steps: - - attach_ejs_workspace - - install_native_deps - - run: make check-stage2 - - build-stage3: - docker: - - image: cimg/node:20.6.1 - steps: - - attach_ejs_workspace - - install_native_deps - - run: make stage3 - - persist_to_workspace: - root: . - paths: - - ejs.exe.stage3 - - check-stage3: - docker: - - image: cimg/node:20.6.1 - steps: - - attach_ejs_workspace - - install_native_deps - - run: make check-stage3 - -workflows: - build: - jobs: - - setup - - build-stage0: - requires: - - setup - - check-stage0: - requires: - - build-stage0 - - build-stage1: - requires: - - build-stage0 - # - check-stage1: - # requires: - # - build-stage1 - - build-stage2: - requires: - - build-stage1 - # - check-stage2: - # requires: - # - build-stage2 - - build-stage3: - requires: - - build-stage2 - # - check-stage3: - # requires: - # - build-stage3 - # - package: - # requires: - # - check-stage0 - # - check-stage1 - # - check-stage2 - # - check-stage3 diff --git a/.github/workflows/bootstrap.yml b/.github/workflows/bootstrap.yml new file mode 100644 index 00000000..d7d9fda2 --- /dev/null +++ b/.github/workflows/bootstrap.yml @@ -0,0 +1,199 @@ +# The full buck2 bootstrap matrix, as a reusable workflow: ci.yml runs +# it on every push/PR, release.yml runs the SAME jobs on a version tag +# (release-P3's "a release is a green matrix" is literal — one +# definition, two callers). +# +# Per platform (macOS arm64, Linux arm64/x86_64), sequential targets — +# buck2 shares artifacts between them, so the stage ladder (stage1 +# builds feed stage2/3) costs one traversal: +# +# test-eir EIR unit tests (node-hosted) +# test-stage0 full suite against the node-hosted compiler +# test-stage1 suite against the self-compiled compiler +# test-stage2 suite against stage1's self-compile +# test-stage3 suite + the stage2/stage3 byte-identity fixed point +# +# then the dist tarball + its smoke tests (release-P1) and the package +# smokes (release-P2), uploading echojs-dist- artifacts. +name: bootstrap + +on: + workflow_call: + +jobs: + bootstrap-macos-arm64: + name: bootstrap-macos-arm64 + runs-on: macos-15 # arm64 + timeout-minutes: 120 + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install llvm + run: | + brew install llvm + "$(brew --prefix llvm)/bin/llvm-config" --version + + # the facebook/buck2 `latest` release binary, same as before — + # the action just owns the platform selection and unpacking + - uses: dtolnay/install-buck2@latest + - run: buck2 --version + + # unpinned (runtime-P3): the value-based harness serializes logged + # values itself (test/harness-console-shim.js) on both the node and + # ejs sides, so baselines no longer depend on node's inspect format + # (verified: 22.4.0 and 22.23.2 generate byte-identical baselines) + - uses: actions/setup-node@v4 + with: + node-version: 22.x + + - name: npm ci + run: npm ci + + # the node-hosted (stage0) compiler drives llvm through this + # node-gyp native addon; buck picks up the built artifact + - name: Build the node-llvm addon + run: ./node-llvm/build-addon.sh "$(brew --prefix llvm)" + + - name: TypeScript typecheck + run: | + node node_modules/typescript/bin/tsc -p tsconfig.json + node node_modules/typescript/bin/tsc -p test --noEmit + + - name: buck2 bootstrap matrix + run: | + buck2 build \ + //:test-eir \ + //:test-stage0 \ + //:test-stage1 \ + //:test-stage2 \ + //:test-stage3 + + # the relocatable dist artifact + its installed-layout smoke test + # (release-P1); the stage builds above are shared, so this only + # adds the repack + smoke compile + - name: dist artifact + run: | + buck2 build //:test-dist + buck2 build //:dist --out dist-out/ + + - uses: actions/upload-artifact@v4 + with: + name: echojs-dist-macos-arm64 + path: dist-out/*.tar.gz + if-no-files-found: error + + # release-P2 package smokes. The formula comes from the tarball + # just built (file:// url) via a throwaway local tap; the npm + # wrapper installs through its EJS_NPM_TARBALL override. + # release-P3 points both at hosted release assets instead. + - name: package smoke (homebrew + npm) + run: | + brew tap-new --no-git toshok/echojs-ci + ./packaging/homebrew/make-formula.sh \ + --tarball dist-out/echojs-*.tar.gz \ + --out "$(brew --repository)/Library/Taps/toshok/homebrew-echojs-ci/Formula/echojs.rb" + brew install toshok/echojs-ci/echojs + echo 'console.log(`ok ${[1,2].map((x) => x + 1).join("")}`)' > "$RUNNER_TEMP/smoke.js" + "$(brew --prefix)/bin/ejs" -q -o "$RUNNER_TEMP/smoke.exe" "$RUNNER_TEMP/smoke.js" + test "$("$RUNNER_TEMP/smoke.exe")" = "ok 23" + brew test echojs + brew uninstall echojs + + cd "$RUNNER_TEMP" + npm pack "$GITHUB_WORKSPACE/packaging/npm" + mkdir npm-smoke && cd npm-smoke + npm init -y > /dev/null + EJS_NPM_TARBALL="$(echo "$GITHUB_WORKSPACE"/dist-out/echojs-*.tar.gz)" \ + npm install --no-fund --no-audit ../pirouette-echojs-*.tgz + ./node_modules/.bin/ejs -q -o smoke.exe ../smoke.js + test "$(./smoke.exe)" = "ok 23" + + - name: Surface test logs on failure + if: failure() + run: | + find buck-out/v2 -name "test-*.log" -newer package.json 2>/dev/null | while read -r f; do + echo "=== $f ===" + tail -60 "$f" + done || true + + bootstrap-linux: + name: bootstrap-linux-${{ matrix.arch }} + runs-on: ${{ matrix.runner }} + timeout-minutes: 150 + strategy: + fail-fast: false + matrix: + include: + - arch: arm64 + runner: ubuntu-24.04-arm + - arch: x86_64 + runner: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install packages + run: | + sudo apt-get update -qq + sudo apt-get install -y -qq build-essential cmake libunwind-dev libuv1-dev + + - name: Install llvm 22 + run: | + curl -sSf https://apt.llvm.org/llvm.sh -o /tmp/llvm.sh + chmod +x /tmp/llvm.sh + sudo /tmp/llvm.sh 22 + /usr/lib/llvm-22/bin/llvm-config --version + # prelude's cxx toolchain wants a bare clang++ on PATH + echo "/usr/lib/llvm-22/bin" >> "$GITHUB_PATH" + + - uses: dtolnay/install-buck2@latest + - run: buck2 --version + + # unpinned (runtime-P3) — see the macOS job's note + - uses: actions/setup-node@v4 + with: + node-version: 22.x + + - name: npm ci + run: npm ci + + - name: Build the node-llvm addon + run: ./node-llvm/build-addon.sh /usr/lib/llvm-22 + + - name: buck2 bootstrap matrix + run: | + buck2 build --config llvm.prefix=/usr/lib/llvm-22 \ + //:test-eir \ + //:test-stage0 \ + //:test-stage1 \ + //:test-stage2 \ + //:test-stage3 + + # release-P1 — see the macOS job's note + - name: dist artifact + run: | + buck2 build --config llvm.prefix=/usr/lib/llvm-22 //:test-dist + buck2 build --config llvm.prefix=/usr/lib/llvm-22 //:dist --out dist-out/ + + - uses: actions/upload-artifact@v4 + with: + name: echojs-dist-linux-${{ matrix.arch }} + path: dist-out/*.tar.gz + if-no-files-found: error + + # release-P2 — the npm wrapper against the tarball just built + # (the prefix installer is smoke-tested inside //:test-dist) + - name: package smoke (npm) + run: | + cd "$RUNNER_TEMP" + npm pack "$GITHUB_WORKSPACE/packaging/npm" + mkdir npm-smoke && cd npm-smoke + npm init -y > /dev/null + EJS_NPM_TARBALL="$(echo "$GITHUB_WORKSPACE"/dist-out/echojs-*.tar.gz)" \ + npm install --no-fund --no-audit ../pirouette-echojs-*.tgz + echo 'console.log(`ok ${[1,2].map((x) => x + 1).join("")}`)' > smoke.js + ./node_modules/.bin/ejs -q -o smoke.exe smoke.js + test "$(./smoke.exe)" = "ok 23" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..9300fd17 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,17 @@ +# EchoJS CI: the full buck2 bootstrap matrix on every push/PR. The +# jobs live in bootstrap.yml (a reusable workflow) so release.yml can +# run the identical matrix on a version tag. +name: CI + +on: + push: + branches: [main, eir] + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + bootstrap: + uses: ./.github/workflows/bootstrap.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..4135e53f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,214 @@ +# EchoJS release pipeline (release-P3). Runs when a v tag is +# pushed (prepare-release.sh makes the tag; pushing it is the human +# act that starts this). A release is: +# +# version-check the tag, package.json, the npm wrapper, and the +# CHANGELOG all agree +# bootstrap the SAME full matrix CI runs (bootstrap.yml): +# stage ladder + dist tarballs + package smokes on +# all three platforms +# publish a DRAFT GitHub release holding the three tarballs, +# the generated homebrew formula (hosted urls), and +# the npm wrapper tgz; changelog section = notes. +# Publishing the draft is the go-live act (draft +# asset urls are not public, so the formula and npm +# postinstall only resolve once it's published). +# The tap push runs iff HOMEBREW_TAP_TOKEN is +# configured. npm publish uses OIDC trusted +# publishing (docs.npmjs.com/trusted-publishers) — +# no token; npmjs trusts THIS workflow file +# (owner/repo + filename release.yml are what the +# trusted-publisher config matches, so renaming +# this file breaks publishing) — gated on the +# NPM_TRUSTED_PUBLISHING repo variable being "true" +# so releases stay green until the publisher is +# configured on npmjs.com. +# smoke-* clean-machine proof: a bare ubuntu container and a +# fresh macos runner install ONLY the tarball + the +# documented prerequisites, then compile and run a +# program through the installed layout. +name: Release + +on: + push: + tags: ["v[0-9]+.[0-9]+.[0-9]+"] + +permissions: + contents: write + +concurrency: + group: release-${{ github.ref }} + +jobs: + version-check: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - name: tag agrees with the tree + run: | + TAG="${GITHUB_REF_NAME#v}" + for f in package.json packaging/npm/package.json; do + V="$(node -p "require('./$f').version")" + if [ "$V" != "$TAG" ]; then + echo "::error::$f has version $V but the tag says $TAG (use packaging/prepare-release.sh)" + exit 1 + fi + done + if ! grep -q "^## \[$TAG\] " CHANGELOG.md; then + echo "::error::CHANGELOG.md has no '## [$TAG]' section" + exit 1 + fi + + bootstrap: + needs: version-check + uses: ./.github/workflows/bootstrap.yml + + publish: + needs: bootstrap + runs-on: ubuntu-24.04 + permissions: + contents: write + id-token: write # OIDC token for npm trusted publishing + steps: + - uses: actions/checkout@v4 + + # trusted publishing needs npm >= 11.5.1 on node >= 22.14 + - uses: actions/setup-node@v4 + with: + node-version: 24 + - run: npm install -g npm@latest && npm --version + + - uses: actions/download-artifact@v4 + with: + pattern: echojs-dist-* + merge-multiple: true + path: assets + + - name: assemble release assets + run: | + TAG="${GITHUB_REF_NAME#v}" + ls -l assets + test -f "assets/echojs-$TAG-arm64-macos.tar.gz" + test -f "assets/echojs-$TAG-arm64-linux.tar.gz" + test -f "assets/echojs-$TAG-x86_64-linux.tar.gz" + + # homebrew formula: sha from the built tarball, url = the + # published asset location + ./packaging/homebrew/make-formula.sh \ + --tarball "assets/echojs-$TAG-arm64-macos.tar.gz" \ + --url "https://github.com/${GITHUB_REPOSITORY}/releases/download/v$TAG/echojs-$TAG-arm64-macos.tar.gz" \ + --out assets/echojs.rb + + # the npm wrapper tgz (version already stamped by + # prepare-release.sh; its postinstall downloads the tarball + # asset for the host platform) + npm pack ./packaging/npm --pack-destination assets + + # release notes = this version's changelog section + awk -v v="$TAG" ' + $0 ~ "^## \\[" v "\\] " { f = 1; next } + /^## \[/ { f = 0 } + f + ' CHANGELOG.md > notes.md + cat notes.md + + - name: draft the GitHub release + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release create "$GITHUB_REF_NAME" \ + --draft \ + --verify-tag \ + --title "echojs ${GITHUB_REF_NAME#v}" \ + --notes-file notes.md \ + assets/* + + # both publish legs are shell-gated on their secrets: absent + # secret = loudly-skipped step, not a broken release + - name: push the formula to the tap + env: + TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} + run: | + if [ -z "$TAP_TOKEN" ]; then + echo "HOMEBREW_TAP_TOKEN not configured — formula is attached to the release instead" + exit 0 + fi + git clone "https://x-access-token:${TAP_TOKEN}@github.com/toshok/homebrew-echojs.git" tap + mkdir -p tap/Formula + cp assets/echojs.rb tap/Formula/echojs.rb + cd tap + git add Formula/echojs.rb + git -c user.name="echojs release" -c user.email="toshok@gmail.com" \ + commit -m "echojs ${GITHUB_REF_NAME#v}" + git push + + # OIDC trusted publishing: no token, npm exchanges this job's + # id-token for short-lived credentials and generates provenance + # attestations automatically. Publishes the package DIRECTORY + # (same bits as the attached tgz — both come from packaging/npm + # at this ref) so provenance sees the build context. + - name: npm publish (trusted publishing) + env: + TP_ENABLED: ${{ vars.NPM_TRUSTED_PUBLISHING }} + run: | + if [ "$TP_ENABLED" != "true" ]; then + echo "NPM_TRUSTED_PUBLISHING repo variable not set — wrapper tgz is attached to the release instead" + echo "(configure the trusted publisher on npmjs.com first: owner toshok, repo echojs, workflow release.yml)" + exit 0 + fi + npm publish ./packaging/npm --access public + + # a machine that has never seen the repo: only the tarball + the + # README's documented prerequisites + smoke-linux: + needs: bootstrap + strategy: + fail-fast: false + matrix: + include: + - arch: arm64 + runner: ubuntu-24.04-arm + - arch: x86_64 + runner: ubuntu-24.04 + runs-on: ${{ matrix.runner }} + container: ubuntu:24.04 + steps: + - uses: actions/download-artifact@v4 + with: + name: echojs-dist-linux-${{ matrix.arch }} + + - name: install prerequisites (the README's list) + run: | + apt-get update -qq + apt-get install -y -qq curl ca-certificates gnupg lsb-release \ + software-properties-common build-essential libuv1-dev libunwind-dev + curl -sSf https://apt.llvm.org/llvm.sh -o /tmp/llvm.sh + chmod +x /tmp/llvm.sh + /tmp/llvm.sh 22 + + - name: install and compile + run: | + tar xzf echojs-*.tar.gz + sh ./echojs-*/install.sh + printf 'let xs = [1, 2, 3].map((x) => x * x);\nconsole.log(`squares: ${xs.join(",")}`);\n' > hello.js + PATH="/usr/lib/llvm-22/bin:$PATH" ejs -q -o hello hello.js + test "$(./hello)" = "squares: 1,4,9" + + smoke-macos: + needs: bootstrap + runs-on: macos-15 + steps: + - uses: actions/download-artifact@v4 + with: + name: echojs-dist-macos-arm64 + + - name: install prerequisites (the README's list) + run: brew install llvm + + - name: install and compile + run: | + tar xzf echojs-*.tar.gz + sh ./echojs-*/install.sh --prefix "$RUNNER_TEMP/prefix" + printf 'let xs = [1, 2, 3].map((x) => x * x);\nconsole.log(`squares: ${xs.join(",")}`);\n' > hello.js + "$RUNNER_TEMP/prefix/bin/ejs" -q -o hello hello.js + test "$(./hello)" = "squares: 1,4,9" diff --git a/.gitignore b/.gitignore index 5029811f..11f49ee6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +/buck-out +.vscode/ **/.DS_Store *.slo *.lo @@ -11,7 +13,7 @@ *.s *.exe -ejs.js +/ejs.js ejs.exe ejs.exe.stage1 ejs.exe.stage2 @@ -21,3 +23,10 @@ echojs-*.tar.gz node_modules/ .stamp-* + +# tsc output of test/tester.ts (buck-test-stage.sh compiles it when +# staging; hand-runs compile it in place) +test/tester.js + +# assembled stage0-style work trees (maam diff harness / --types diff lane) +maam-difftree/ diff --git a/.gitmodules b/.gitmodules index 27e3ef7b..ed2e4bef 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,7 @@ +[submodule "echojs-maam"] + path = external-deps/echojs-maam + url = https://github.com/toshok/echojs-maam.git + branch = ejs-integration [submodule "esprima"] path = external-deps/esprima url = https://github.com/toshok/esprima.git @@ -22,3 +26,6 @@ [submodule "external-deps/double-conversion"] path = external-deps/double-conversion url = https://github.com/google/double-conversion.git +[submodule "prelude"] + path = prelude + url = https://github.com/facebook/buck2-prelude.git diff --git a/BUCK b/BUCK new file mode 100644 index 00000000..4685119e --- /dev/null +++ b/BUCK @@ -0,0 +1,174 @@ +load("//:defs.bzl", "EJS_OS", "EJS_SHORT_TRIPLE", "EJS_TRIPLE", "llvm_bindir") + +platform( + name = "linux-x86_64", + constraint_values = [ + "@config//os/constraints:linux", + "@config//cpu/constraints:x86_64", + ], +) + +platform( + name = "macos-arm64", + constraint_values = [ + "@config//os/constraints:macos", + "@config//cpu/constraints:arm64", + ], +) + +export_file( + name = "ejs-es6.ts", + visibility = ["PUBLIC"], +) + +# A directory laid out the way `ejs --srcdir` expects a source checkout to +# look, containing everything needed to self-compile the compiler. +genrule( + name = "srcdir-tree", + srcs = ["buck-srcdir-tree.sh"], + out = "root", + cmd = "bash $SRCDIR/buck-srcdir-tree.sh" + + ' "' + EJS_TRIPLE + '"' + + ' "' + EJS_SHORT_TRIPLE + '"' + + ' "' + EJS_OS + '"' + + ' "$(location //runtime:headers)"' + + ' "$(location //runtime:echo[static])"' + + ' "$(location //runtime:platform-icc-o)"' + + ' "$(location //external-deps:pcre-build[lib])"' + + ' "$(location //external-deps:double-conversion-build)"' + + ' "$(location //external-deps:compiler-js)"' + + ' "$(location //lib:tsjs)"' + + ' "$(location //lib:host-config.js)"' + + ' "$(location //lib:tsjs)"' + + ' "$(location //node-compat:node-compat.ejs)"' + + ' "$(location //node-compat:node-compat[static])"' + + ' "$(location //ejs-llvm:ejs-llvm.ejs)"' + + ' "$(location //ejs-llvm:ejs-llvm[static])"' + + ' "$(location //runtime:echo-dtoa[static])"' + + select({ + "DEFAULT": " -", + "config//os:macos": ' "$(location //runtime:echo-objc[static])"', + }), +) + +# stage1: the generated (CommonJS) compiler running under node (with the +# node-llvm addon) compiles ejs-es6.js to a native executable. +genrule( + name = "ejs.exe.stage1", + srcs = ["buck-stage.sh"], + out = "ejs.exe.stage1", + cmd = 'bash $SRCDIR/buck-stage.sh "$(location :srcdir-tree)" node ' + + '"$(location //lib:generated)" "$(location //node-llvm:llvm.node)" ' + + llvm_bindir(), +) + +# stage2: stage1 compiles the compiler. +genrule( + name = "ejs.exe.stage2", + srcs = ["buck-stage.sh"], + out = "ejs.exe.stage2", + cmd = 'bash $SRCDIR/buck-stage.sh "$(location :srcdir-tree)" exe ' + + '"$(location :ejs.exe.stage1)" - ' + llvm_bindir(), +) + +# stage3: stage2 compiles the compiler; stage2 and stage3 should be +# functionally identical if the bootstrap is healthy. +genrule( + name = "ejs.exe.stage3", + srcs = ["buck-stage.sh"], + out = "ejs.exe.stage3", + cmd = 'bash $SRCDIR/buck-stage.sh "$(location :srcdir-tree)" exe ' + + '"$(location :ejs.exe.stage2)" - ' + llvm_bindir(), +) + +# `make` (all) builds stage1 and installs it as ejs.exe; mirror that. +alias( + name = "ejs.exe", + actual = ":ejs.exe.stage1", +) + +# the relocatable dist artifact (release-P1): the installed layout the +# driver's non---srcdir mode expects, tarred up. The stage2 binary is +# the one the bootstrap fixed point (stage3) vouches for. +# buck2 build //:dist +genrule( + name = "dist", + srcs = [ + "buck-dist.sh", + "package.json", + "LICENSE.txt", + "packaging/install.sh", + ], + out = "dist", + cmd = 'bash $SRCDIR/buck-dist.sh "$(location :srcdir-tree)" ' + + '"$(location :ejs.exe.stage2)"' + + ' "' + EJS_TRIPLE + '"' + + ' "' + EJS_SHORT_TRIPLE + '"' + + ' "' + EJS_OS + '"' + + ' "$SRCDIR/package.json" "$SRCDIR/LICENSE.txt"' + + ' "$SRCDIR/packaging/install.sh"', +) + +# smoke-test the dist artifact as a user would use it: unpack, compile +# and run programs WITHOUT --srcdir, and check the fail-loudly LLVM +# policy. buck2 build //:test-dist +genrule( + name = "test-dist", + srcs = ["buck-test-dist.sh"], + out = "test-dist.log", + cmd = 'bash $SRCDIR/buck-test-dist.sh "$(location :dist)" ' + llvm_bindir(), +) + +# EIR unit tests (run under node against the generated CommonJS tree): +# buck2 build //:test-eir +genrule( + name = "test-eir", + out = "test-eir.log", + cmd = '(node "$(location //lib:generated)/lib/eir/tests.js" > $OUT 2>&1) || ' + + "{ cat $OUT >&2; exit 1; }; tail -1 $OUT", +) + +# the Phase 2 low-tier end-to-end probe: stage0-compile test/eir-lowtier1.js +# with -flowtier (hand-built low-tier bodies) and check output + +# emitted IR: buck2 build //:test-eir-lowtier +genrule( + name = "test-eir-lowtier", + srcs = ["buck-test-lowtier.sh"], + out = "test-eir-lowtier.log", + cmd = 'bash $SRCDIR/buck-test-lowtier.sh "$(location :srcdir-tree)" ' + + '"$(location //lib:generated)" "$(location //test:files)" ' + llvm_bindir(), +) + +# run the test suite against a stage: buck2 build //:test-stage3 +# the output artifact is the full test log; the build fails if any test +# fails. +genrule( + name = "test-stage0", + srcs = ["buck-test-stage.sh"], + out = "test-stage0.log", + cmd = 'bash $SRCDIR/buck-test-stage.sh "$(location :srcdir-tree)" ' + + '"$(location //lib:generated)" - 0 "$(location //test:files)" ' + llvm_bindir(), +) + +[ + genrule( + name = "test-stage" + stage, + srcs = ["buck-test-stage.sh"], + out = "test-stage" + stage + ".log", + cmd = 'bash $SRCDIR/buck-test-stage.sh "$(location :srcdir-tree)" ' + + '"$(location //lib:generated)" "$(location :ejs.exe.stage' + stage + ')" ' + + stage + ' "$(location //test:files)" ' + llvm_bindir(), + ) + for stage in ["1", "2", "3"] +] + +# the runtime shapes A/B lane (shapes-plan P4.1): the full stage1 suite +# with shape tracking disabled must be just as green as the default run +genrule( + name = "test-stage1-shapes-off", + srcs = ["buck-test-stage.sh"], + out = "test-stage1-shapes-off.log", + cmd = 'bash $SRCDIR/buck-test-stage.sh "$(location :srcdir-tree)" ' + + '"$(location //lib:generated)" "$(location :ejs.exe.stage1)" ' + + '1 "$(location //test:files)" ' + llvm_bindir() + ' "" "EJS_SHAPES=off"', +) diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..0608603e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,50 @@ +# Changelog + +All notable changes to echojs are recorded here, newest first. The +format follows [Keep a Changelog](https://keepachangelog.com); versions +follow [semver](https://semver.org) with the pre-1.0 reading: while the +major is 0, a minor bump may break things and a patch bump may not. + +Releases are cut by `packaging/prepare-release.sh `, which +rolls the Unreleased section into a dated version section, stamps the +version everywhere it lives, and tags `v`; pushing the tag +runs the release pipeline (a release is a green bootstrap matrix + +packaged artifacts + a clean-machine install smoke — see +`.github/workflows/release.yml`). + +## [Unreleased] + +### Added + +- The EIR compilation pipeline: an SSA IR between the AST and LLVM, + with a clang-style pass configuration (`-O0`..`-O3` suites, + `-f`/`-fno-` per-pass flags, `--print-passes`). +- Type-feedback optimization: shape tracking, guarded fast paths, + born-with-shape allocation, typed slots, allocation sinking, + devirtualization, and an export-boundary specialization wrapper. +- A generational, mostly-copying garbage collector with compaction, + precise young-generation roots from compiler-emitted gc-frames, and + `EJS_GC_*` debugging knobs. +- Relocatable per-platform dist tarballs (macOS arm64, Linux + arm64/x86_64) with a bundled prefix installer, a Homebrew formula + generator, and an npm wrapper package (`@pirouette/echojs`). +- An LLVM toolchain policy: the driver discovers a matching-major + `opt`/`llc` (env `LLVM_BINDIR` override → build-baked path → + conventional locations → PATH) and refuses to run against a + different major. +- A value-based test harness whose baselines are independent of the + node version, and a fully self-hosted bootstrap proven by a + four-stage CI matrix on all three platforms. + +### Changed + +- The compiler sources are TypeScript throughout; babel is gone from + the toolchain (one `tsc` pass converts modules for the build). + +### Fixed + +- Too many runtime-correctness fixes to enumerate here (typeof null, + -0 semantics, Math.round ties, string-to-number edge cases, sparse + arrays, generator exception propagation, error prototype chains, + DataView indexing, and more) — see docs/runtime-p1-results.md and + docs/runtime-p3-results.md. diff --git a/EIRProposal.md b/EIRProposal.md new file mode 100644 index 00000000..0bd0da40 --- /dev/null +++ b/EIRProposal.md @@ -0,0 +1,338 @@ +# EIR: a proposal for an EchoJS intermediate representation + +This proposes replacing the compiler's AST+intrinsics middle-end with a +dedicated SSA IR ("EIR") that sits between the desugaring passes and LLVM +emission. It is motivated by two concrete needs: + +1. giving language-level optimizations a place to live (LLVM only sees + opaque `_ejs_op_*` calls and can't reason about JS semantics), and +2. giving the abstract-interpretation static analysis effort a real + dataflow substrate (CFG + SSA + effect annotations) instead of an AST. + +## Why the current architecture fights us + +Today's pipeline is: + + esprima AST + → ~20 desugaring passes (AST → AST) + → closure conversion (AST → AST + %intrinsic pseudo-calls) + → LLVMIRVisitor (AST → LLVM IR, allocas everywhere) + → llvm-as / opt -O2 / llc + +Three structural problems fall out of this: + +**The middle-end has no vocabulary of its own.** Semantic operations are +encoded as `CallExpression`s with magic callee names (`%moduleGetSlot`, +`%slot`, `%makeClosure`, `%invokeClosure`, `%typeofIsObject`, ...) — +new-cc.js alone has ~50 `intrinsic(...)` construction sites. Passes that +want to reason about these ops have to pattern-match call expressions +(`is_intrinsic(n.object, "%moduleGetExotic")`), and nothing checks that +an intrinsic's arguments are well-formed until LLVMIRVisitor throws (or +worse, silently miscompiles — several of the bugs fixed during the +bootstrap work were of exactly this shape: the for-of/destructuring pass +ordering bug, the module-slot layout bug, the `is32bit` truthiness bug). + +**All dataflow is outsourced to mem2reg.** Every local lives in an +entry-block `alloca` (`createAllocas` comments: "so the mem2reg opt pass +can regenerate the ssa form for us"), and every assignment is a +store/load pair. That's fine as far as LLVM is concerned — clang does +the same — but it means *we* never hold an SSA view of the program. By +the time SSA exists, the program is LLVM IR where `a + b` is an opaque +call to `_ejs_op_add` and a property access is `_ejs_object_getprop`. +LLVM can CSE neither, can't fold `typeof x === "string"` after a guard, +can't sink a boxing operation, can't stack-allocate a closure env that +doesn't escape. Everything that requires knowing JS semantics is +currently optimized by nobody. + +**The AST is a poor substrate for abstract interpretation.** A +fixed-point dataflow analysis wants a CFG with explicit joins, values +with single definitions to attach lattice facts to, and effect summaries +per operation. Deriving all of that on the fly from an AST (with +exitable-scope's implicit control flow, `arguments` aliasing, and +intrinsic-calls-as-expressions) means the analysis re-implements half a +compiler front-end before it can begin. + +So: yes, I think this is the right move. The honest caveat is that +alloca+mem2reg is *not* a performance problem by itself — the win is not +"skip mem2reg", it's everything a real IR unlocks: JS-aware optimization, +a shared substrate with the analysis work, verifiability, and the +deletion of the intrinsics-through-AST encoding. Direct SSA emission is +then a pleasant side effect of already being in SSA. + +## Design + +### Shape + +MLIR/Cranelift-flavored, not LLVM-flavored, in one specific way: **basic +block arguments instead of phi nodes**. Block args are easier to build +directly from an AST, easier to verify, and dramatically nicer for an +abstract interpreter (a join point's values are just the block's +parameters — no "which predecessor am I" bookkeeping). They translate to +LLVM phis mechanically at emission. + + module := function*, module-metadata (imports, exports, slot table) + function := name, params, blocks, env-shape + block := label, block-args, instruction*, terminator + instruction := result? = opcode operand*, attributes + terminator := br / cond_br / switch / return / throw / unreachable + (call-like instructions may also terminate: see EH) + +Values are typed. The type lattice is EchoJS's, not LLVM's: + + any -- a boxed ejsval, contents unknown + ├─ number (⊇ int32) -- still boxed; refinement facts + ├─ string, symbol + ├─ boolean, undefined, null + ├─ object (optionally: object, array, function) + raw types: f64, i32, b1, rawptr, rawptr -- unboxed, post-lowering + +A value of type `number` is still an ejsval at the `any` level of the IR; +the type is a *fact*, not a representation. Representation change is an +explicit instruction (`unbox_f64` / `box_f64`), introduced by lowering. + +### Two tiers, one IR + +Rather than two separate IRs, EIR has high-level and low-level opcodes in +one instruction set, and a lowering pass between them (SpiderMonkey +MIR/LIR and V8's ignition→turbofan pipelines both converged on something +similar; for a two-person project one IR with tiers is much cheaper). + +**High tier** — one opcode per semantic operation the language has. +Everything the ~50 AST intrinsics encode today becomes a first-class, +verifiable instruction: + + %v = add %a, %b ; generic JS +, may throw (valueOf) + %v = get_prop %obj, %key ; may throw, reads heap + set_prop %obj, %key, %v + %v = get_prop_atom %obj, atom(length) + %f = make_closure fn(@inner), %env + %e = make_env 3, parent=%env0 ; env with 3 slots + %v = env_load %e, slot(2) + env_store %e, slot(2), %v + %v = module_slot_load module(lib/consts), slot(46) + module_slot_store module(...), slot(n), %v + %v = call %callee, this=%t, args(%a, %b) + %v = construct %callee, args(...) + %b = to_boolean %v + %b = typeof_is %v, "string" ; pure + %b = strict_eq %a, %b ; pure + %v = const ejsval(atom "Program") ; pure + ... + +Every opcode carries an **effect signature** in a static table: +`{pure | reads-heap | writes-heap} × {may-throw} × {may-gc} × {may-call}`. +This table is the contract the optimizer *and* the abstract interpreter +both consume — it is the single most valuable artifact of the whole +design, and it's about 60 lines. + +**Low tier** — what emission actually wants: tag tests, unboxing, raw +arithmetic, direct runtime calls: + + %t = has_tag %v, double-tag ; pure, b1 + %d = unbox_f64 %v ; pure (requires proven tag) + %r = f64.add %d1, %d2 + %v2 = box_f64 %r + %v = call_runtime _ejs_op_add(%a, %b) ; the fallback the high op lowers to + +The lowering pass maps each high op to either (a) a guarded fast path + +runtime-call slow path, or (b) a plain runtime call — *informed by the +type facts on its operands*. This is the hook where the abstract +interpreter pays rent: it runs on the high tier, refines operand types +(`%a: number`, `%b: number`), and lowering then emits `f64.add` with no +guards instead of `call _ejs_op_add`. Today there is no place in the +pipeline where that transaction can even be expressed. + +### Control flow and exceptions + +All control flow is explicit edges between blocks. `exitable-scope.js`'s +implicit break/continue/return-through-finally machinery disappears into +ordinary CFG construction (finally blocks are duplicated or dispatched at +lowering-from-AST time, exactly once, in one place). + +Exceptions use LLVM's model, because we must emit it anyway: any +`may-throw` instruction inside a protected region becomes a terminator +with two successors: + + %v = invoke get_prop %obj, %key + normal ^bb7(%v), unwind ^catch3(%exc) + +Blocks reached by unwind edges are catch blocks; their block-arg is the +caught value. Emission maps this 1:1 onto invoke/landingpad with the +existing EJS personality. Outside protected regions, may-throw +instructions are plain instructions (unwinding propagates), same as +today. + +### Functions, closures, environments + +Closure conversion moves from an AST pass (new-cc, ~1500 lines of the +subtlest code in the compiler) into the AST→EIR lowering: scope +resolution assigns each binding to a param, an SSA local, or an env slot, +and emits `make_env`/`env_load`/`env_store` directly. Because envs and +slots are first-class instructions with known effects, two optimizations +become straightforward EIR passes later: + +- **env promotion**: a captured-but-never-mutated-after-capture slot's + loads can be forwarded to the stored value; an env whose closure never + escapes can be elided entirely (today every function with any capture + allocates a GC'd env unconditionally); +- **direct calls**: `%f = make_closure fn(@inner), %e` followed by + `call %f` can become a direct call to `@inner` with `%e` passed + explicitly, skipping `_ejs_invoke_closure`'s dispatch. + +The GC contract stays exactly as the runtime now guarantees it: ejsvals +and raw env/object pointers live in SSA values → machine registers/stack +slots, which the conservative scanner already handles (including interior +pointers, as of the recent GC work). No stack maps needed. The one rule +EIR must enforce (verifier-checked): a raw *derived* pointer may not be +live across a `may-gc` instruction unless the base is also live — which +the conservative scanner then makes safe. + +### Textual format + +Every function above implies it: EIR has a canonical textual form, parsed +and printed by the compiler. This is load-bearing, not cosmetic — golden +tests for lowering, a `--emit-eir` flag for debugging, serialization for +the analysis tooling, and reduced repro cases all come from it. + +Example — `function inc(x) { return x + 1; }` after lowering + analysis +proved nothing about `x`: + + fn @inc(%this: any, %x: any) -> any { + ^entry: + %c1 = const number(1) + %r = add %x, %c1 ; may-throw, may-gc + return %r + } + +after the abstract interpreter proves `%x: number` at all call sites: + + fn @inc(%this: any, %x: any but-known number) -> any { + ^entry: + %d = unbox_f64 %x + %r = f64.add %d, 1.0 + %v = box_f64 %r + return %v + } + +### Block-argument-driven specialization (basic block versioning) + +Block arguments make one further strategy available that phi-form SSA +makes awkward: **specializing blocks on the types of their arguments**. +A block is a small function of its parameters; if analysis (or profiling) +shows a block is entered with `(number, string)` on one edge and +`(any, any)` on another, the lowering can *version* the block — clone it +per distinct argument-type tuple, wiring each predecessor edge to the +version matching the types it can prove it passes. Inside a version, +the parameter types are facts, so guards disappear and unboxing floats +to the block entry. This is Chevalier-Boisvert & Feeley's basic block +versioning (ECOOP'15), which gets most of the benefit of interprocedural +type inference at a fraction of the implementation cost, and it consumes +exactly the interface EIR already has: types attached to block +parameters, edges that pass arguments. The static analysis can treat a +block as its unit of work — a lattice tuple in through the parameters, +facts out through the terminator's edges — and versioning is then a +lowering decision, not an analysis one. (A version cap per block, ~4 in +the literature, bounds code growth.) + +### SSA construction + +Build SSA *during* AST→EIR lowering with the Braun/Buchwald/Hack +algorithm ("Simple and Efficient Construction of SSA Form", CC'13): local +value numbering per block + lazy block-arg insertion on demand, no +dominator computation, designed exactly for AST-to-SSA translation, and +small enough to implement in a few hundred lines of the JS we can +self-host. (This matters: the compiler compiles itself, so the IR +implementation must be written in the subset of JS EchoJS handles, and +compile-time performance of the compiler is a user-visible cost.) + +### What the abstract interpreter gets + +- CFG with block args → textbook fixed-point iteration, join = block + entry, no SSA-deconstruction shims; +- one definition per value → lattice facts keyed by value id, stored in a + side table (the IR never mutates for analysis); +- the effect table → sound handling of calls/heap without re-deriving + behavior from op names; +- module metadata (export slots, const-ness — gather-imports already + computes `constval`) → interprocedural constants for free; +- the textual format → corpus capture and regression fixtures. + +The contract between the two efforts is intentionally thin: the analysis +consumes high-tier EIR + the effect table, and produces a side table of +`value-id → lattice fact` (plus optionally `call-site → callee set`). +Lowering consumes that side table. Neither needs the other to exist to +make progress: lowering without facts just always takes the generic +path, which is exactly today's behavior. + +## What EIR replaces, and what it doesn't + +Unchanged: esprima, all the *syntactic* desugaring passes (classes, +destructuring, for-of, generators, arguments, templates...), the runtime, +llc/linking. Desugars are cheap, well-understood, and testable; EIR +should receive a maximally-desugared AST. + +Replaced, eventually: `new-cc.js` (closure conversion → lowering), +`exitable-scope.js` (→ CFG construction), `compiler.js`'s LLVMIRVisitor +(→ a much smaller EIR→LLVM emitter: every EIR value is an LLVM value, +block args are phis, invoke edges are invokes — no allocas except the +few real ones: `arguments` objects, scratch areas). + +## Migration plan + +The bootstrap gives us an unusually strong safety net: 373 tests × 3 +stages, plus the stage2≡stage3 fixed-point check, which catches +miscompiles of the compiler itself. Use it. + +1. **EIR core** (data structures, builder, verifier, printer/parser, + effect table). Pure addition; no behavior change. Landable and + testable standalone — and immediately usable by the analysis work. +2. **AST→EIR lowering + naive EIR→LLVM emission** behind a flag + (`--ir`), initially only for functions using a whitelisted subset of + constructs (fall back to the legacy path per-function otherwise). + Success = test suite green with the flag on, then fixed point holds. +3. **Grow coverage** until the whitelist is "everything"; make `--ir` + the default; keep legacy for one release as `--legacy-codegen`. +4. **Delete** new-cc/exitable-scope/LLVMIRVisitor; the AST intrinsics + vocabulary disappears with them. +5. **Optimize** (now, not before): env promotion, direct calls, + guard-informed lowering fed by the abstract interpreter, redundant + box/unbox elimination, atom-keyed `get_prop_atom` ICs. + +Phases 1–2 are the risky-design part and are deliberately boring in +behavior; phase 5 is where the payoff lives, and it only starts once the +suite + fixed point protect it. + +## Risks, named + +- **Semantics drift.** LLVMIRVisitor encodes years of "oh right, JS + does *that*". Mitigation: per-function fallback during migration, the + test suite, and porting visitor code case-by-case rather than + rewriting from the spec. +- **Compiler self-hosting perf.** An extra IR costs compile time; + Braun-style construction and arena-ish (array-indexed, not + pointer-soup) IR storage keep it linear. Budget: self-compile time + should stay within ~1.3× of today through phase 3, and win it back in + phase 5 (less work for opt: we can likely drop `opt -O2` to `-O1` once + we do our own scalar cleanup). +- **GC interactions.** The conservative collector makes most of this a + non-issue, but the derived-pointer-liveness rule must be in the + verifier from day one, not discovered the way we discovered the + register-scanning hole. +- **Two-team coupling.** The analysis effort should consume EIR at + phase 1; if the effect table or type lattice is wrong for them, we + want that feedback before phase 3 freezes the design. + +## Alternatives considered + +- **Keep the AST, add annotations** (facts keyed by AST node): cheapest, + but joins/loops have no natural representation, intrinsics stay + stringly-typed, and emission stays alloca-shaped. This is the status + quo with more bookkeeping. +- **Emit better LLVM directly** (skip our own IR, build LLVM SSA with + its own phi construction): removes mem2reg reliance but gives the + analysis nothing (LLVM IR has erased JS semantics — `_ejs_op_add` is + just a call), and ties every analysis/optimization to the llvm binding + API. +- **CPS / sea-of-nodes**: more power than we need, much harder to + implement, print, verify, and self-host. Block-arg SSA is the + sweet spot. diff --git a/Makefile b/Makefile deleted file mode 100644 index 592ae0c1..00000000 --- a/Makefile +++ /dev/null @@ -1,107 +0,0 @@ -TOP=$(shell pwd) - -include $(TOP)/build/config.mk - -SUBDIRS=external-deps node-compat node-llvm ejs-llvm lib runtime - -STAGE1_EXE = ejs.exe.stage1 -STAGE2_EXE = ejs.exe.stage2 -STAGE3_EXE = ejs.exe.stage3 - -# run git submodule magic if somebody is antsy and doesn't type the magic incantation before typing make -all-local:: ensure-submodules - -NODE_PATH?=$(shell $(MAKE) --no-print-directory -C test node-path) - -all-hook:: stage1 - -install-local:: - @$(MKDIR) $(bindir) - $(INSTALL) -c ejs.exe $(bindir)/ejs - -clean-local:: - @rm -f $(STAGE1_EXE) $(STAGE2_EXE) $(STAGE3_EXE) ejs.exe - -TARNAME=$(PRODUCT_name)-$(PRODUCT_VERSION) -TARFILE=$(TARNAME).tar.gz -DISTROOT=$(TOP) -TAR_EXCLUDES= \ - --exclude .circleci \ - --exclude .git \ - --exclude .gitmodules \ - --exclude .gitignore \ - --exclude .deps \ - --exclude host-config.mk \ - --exclude host-config.js \ - --exclude host-config-es6.js \ - --exclude $(TARFILE) \ - --exclude $(TARNAME) -dist-hook:: ensure-submodules - @echo creating $(DISTROOT)/$(TARNAME).tar.gz - @rm -rf $(DISTROOT)/$(TARNAME) - @$(MKDIR) $(DISTROOT)/$(TARNAME) - @COPYFILE_DISABLE=1 tar -c $(TAR_EXCLUDES) * | tar -C $(DISTROOT)/$(TARNAME) -xp - @(cd $(DISTROOT); \ - COPYFILE_DISABLE=1 tar -czf $(TARFILE) $(TARNAME)) - @rm -rf $(DISTROOT)/$(TARNAME) - @ls -l $(DISTROOT)/$(TARFILE) - -check: - @$(MAKE) -C test $@ - -check-%: - @$(MAKE) -C test $@ - -bootstrap: stage3 - -MODULE_DIRS = --moduledir $(TOP)/node-compat --moduledir $(TOP)/ejs-llvm - -lib/generated: - @$(MAKE) -C lib - -stage0: - @echo DONE - -stage1: $(STAGE1_EXE) - @cp $(STAGE1_EXE) ejs.exe - @ls -l ejs.exe - @echo DONE - -stage2: $(STAGE2_EXE) - @cp $(STAGE2_EXE) ejs.exe - @ls -l ejs.exe - @echo DONE - -stage3: $(STAGE3_EXE) - @cp $(STAGE3_EXE) ejs.exe - @ls -l ejs.exe - @echo DONE - -$(STAGE1_EXE): lib/generated - @echo Building stage 1 - @NODE_PATH="$(NODE_PATH)" ./ejs --srcdir --leave-temp $(MODULE_DIRS) ejs-es6.js - @mv ejs-es6.js.exe $@ - -$(STAGE2_EXE): $(STAGE1_EXE) lib/*.js lib/*.js.in - @echo Building stage 2 - @./$(STAGE1_EXE) --srcdir --leave-temp $(MODULE_DIRS) ejs-es6.js - @mv ejs-es6.js.exe $@ - -$(STAGE3_EXE): $(STAGE2_EXE) lib/*.js lib/*.js.in - @echo Building stage 3 - @./$(STAGE2_EXE) --srcdir --leave-temp $(MODULE_DIRS) ejs-es6.js - @mv ejs-es6.js.exe $@ - -echo-command-line: - @echo ./ejs.exe --leave-temp $(MODULE_DIRS) ejs-es6.js - -osx-tarball: - $(MAKE) -C release osx-tarball - -ensure-submodules: - @if [ ! -f pcre/configure.ac ]; then \ - git submodule init; \ - git submodule update; \ - fi - -include $(TOP)/build/build.mk diff --git a/README.md b/README.md index ed20e3c6..b865de73 100644 --- a/README.md +++ b/README.md @@ -10,38 +10,37 @@ Things only build reliably on OSX. I have easy access to other platforms, I jus On OSX -You'll need a couple of external dependencies to get things running: +The build uses [buck2](https://buck2.build). You'll need: 1. node.js -2. llvm 3.6 -3. coffeescript +2. llvm (homebrew's current keg; the path lives in `.buckconfig` under `[llvm] prefix`) +3. buck2 -The following commands should get you from 0 (well, Homebrew and Xcode) to echo-js built: +The following commands should get you from 0 (well, Homebrew and Xcode) to echo-js built and tested: ```sh -$ brew install node -$ brew install llvm -$ export PATH=/usr/local/opt/llvm/bin:$PATH +$ brew install node llvm $ npm install -$ npm install -g node-gyp babel@5.8.8 -$ export MIN_OSX_VERSION=10.8 # only if you're running 10.8, see below -$ export IOS_SDK_VERSION=9.3 # or whatever is installed -$ export LLVM_SUFFIX= # if installed llvm via homebrew, see below $ git submodule init $ git submodule update -$ make +$ ./node-llvm/build-addon.sh # builds the node addon the stage0 compiler uses +$ buck2 build //:ejs.exe # stage1 compiler (node-hosted stage0 compiles ejs-es6.js) +$ buck2 build //:ejs.exe.stage3 # full bootstrap: stage1 -> stage2 -> stage3 +$ buck2 build //:test-stage3 # run the test suite against stage3 ``` -The environment variable `LLVM_SUFFIX` can be set and its value will be appended to the names of all llvm executables (e.g. `llvm-config-3.6` instead of `llvm-config`.) The default is `-3.6`. Change this if you have a different build of -llvm you want to use. Homebrew installs llvm 3.6 executables without the suffix, thus `export LLVM_SUFFIX=`. +Useful targets: -As for `MIN_OSX_VERSION`: homebrew's formula for llvm (3.4, at least. haven't verified with 3.6) doesn't specify a `-mmacosx-version-min=` flag, so it builds to whatever you have on your machine. Node.js's gyp support in node-gyp, however, *does* put a `-mmacosx-version-min=10.5` flag. A mismatch here causes the node-llvm binding to allocate llvm types using incorrect size calculations, and causes all manner of memory corruption. If you're either running 10.5 or 10.9, you can leave the variable unset. Otherwise, set it to the version of OSX you're running. Hopefully some discussion with the homebrew folks will get this fixed upstream. - -both of these variable assignments can be placed in `echo-js/build/config-local.mk`. +- `//:ejs.exe.stage{1,2,3}` — the bootstrap stages (`//:ejs.exe` is an alias for stage1) +- `//:test-stage{1,2,3}` — build a stage and run the test suite (`test/tester.ts`) against it; the build fails if any test fails, and the output artifact is the test log +- `//:srcdir-tree` — the assembled `--srcdir` layout the compiler runs against +If your llvm lives somewhere other than `/opt/homebrew/opt/llvm`, change `[llvm] prefix` in `.buckconfig`. On Linux +The BUCK files carry `config//os:linux` selects for the runtime and deps, but the linux build hasn't been exercised recently. Patches welcome! + But... Why? diff --git a/buck-dist.sh b/buck-dist.sh new file mode 100644 index 00000000..0bf018a1 --- /dev/null +++ b/buck-dist.sh @@ -0,0 +1,101 @@ +#!/bin/bash +# Invoked by //:dist. Repacks the --srcdir tree (whose libraries the +# bootstrap matrix already proved) plus the stage2 executable into the +# relocatable installed layout the driver's non---srcdir mode expects: +# +# bin/ejs the self-hosted compiler (stage2) +# include/*.h runtime headers (-I at the final link) +# lib//libecho.a runtime + pcre + double-conversion +# lib//libpcre16.a +# lib//libdouble-conversion.a +# lib/node-compat.ejs native-module manifest +# lib//libejsnodecompat-module.a +# +# The LLVM tools are NOT vendored: the driver discovers a matching-major +# opt/llc at runtime and fails loudly otherwise (the release-P1 policy; +# see the llvm_bindir() resolution in ejs-es6.ts). +# +# $OUT is a directory holding echojs--.tar.gz +# (version isn't knowable at buck analysis time, so the tarball name +# can't be the genrule out itself). +set -euo pipefail + +TREE="$1" # //:srcdir-tree +EXE="$2" # //:ejs.exe.stage2 +TRIPLE="$3" # Triple.toString(), e.g. arm64-apple-macos +SHORT_TRIPLE="$4" # Triple.toShortString(), e.g. arm64-macos +OSNAME="$5" # macos | linux +PKG_JSON="$6" # //:package.json (version source until release-P3) +LICENSE="$7" # LICENSE.txt +INSTALL_SH="$8" # packaging/install.sh (shipped at the tarball root) + +VERSION="$(sed -n 's/.*"version": *"\([^"]*\)".*/\1/p' "$PKG_JSON" | head -1)" +test -n "$VERSION" +LLVM_MAJOR="$(sed -n "s/.*LLVM_MAJOR = '\([0-9]*\)'.*/\1/p" "$TREE/lib/host-config.js")" +test -n "$LLVM_MAJOR" + +NAME="echojs-$VERSION-$SHORT_TRIPLE" +mkdir -p "$OUT" +ROOT="$TMP/$NAME" +rm -rf "$ROOT" +mkdir -p "$ROOT/bin" "$ROOT/include" "$ROOT/lib/$TRIPLE" "$ROOT/lib/$SHORT_TRIPLE" + +cp "$EXE" "$ROOT/bin/ejs" +chmod +x "$ROOT/bin/ejs" + +# headers: the srcdir tree keeps them at runtime/*.h +cp "$TREE"/runtime/*.h "$ROOT/include/" + +# link-time libraries, in the exact paths target_libecho()/ +# target_extra_libs() resolve relative to bin/ejs +cp "$TREE/runtime/out/$TRIPLE/libecho.a" "$ROOT/lib/$TRIPLE/libecho.a" +cp "$TREE/external-deps/pcre-$OSNAME/.libs/libpcre16.a" "$ROOT/lib/$TRIPLE/libpcre16.a" +cp "$TREE/external-deps/double-conversion-$OSNAME/double-conversion/libdouble-conversion.a" \ + "$ROOT/lib/$TRIPLE/libdouble-conversion.a" + +# the node-compat native module: manifest scanned from lib/, archive +# from lib/-/ (do_final_link's non---srcdir module path). +# ejs-llvm is deliberately left out: its manifest bakes the build +# machine's `llvm-config --ldflags --libs`, and only the bootstrap +# imports @llvm (reusable native modules are compiler-P4 / P9.5). +cp "$TREE/node-compat/node-compat.ejs" "$ROOT/lib/node-compat.ejs" +cp "$TREE/node-compat/libejsnodecompat-module.a" "$ROOT/lib/$SHORT_TRIPLE/libejsnodecompat-module.a" + +cp "$LICENSE" "$ROOT/LICENSE.txt" + +# machine-readable metadata (sh-sourceable) for the packaging layers: +# install.sh, the homebrew formula generator, the npm postinstall +cat > "$ROOT/dist-info" < "$ROOT/README.md" < JS modules the compiler imports +# external-deps/pcre-/.libs/libpcre16.a +# external-deps/double-conversion-/double-conversion/libdouble-conversion.a +# runtime/*.h passed via -I at the final link +# runtime/out//libecho.a runtime + parson + invoke-closure-catch.o +# node-compat/{node-compat.ejs,libejsnodecompat-module.a} +# ejs-llvm/{ejs-llvm.ejs,libejsllvm-module.a} +set -euo pipefail + +TRIPLE="$1" # Triple.toString(), e.g. arm64-apple-macos +SHORT_TRIPLE="$2" # Triple.toShortString(), e.g. arm64-macos +OSNAME="$3" # macos | linux (also selects ar vs libtool merge below) +HDRS="$4" # //runtime:headers +LIBECHO="$5" # //runtime:echo[static] +ICC_O="$6" # //runtime:platform-icc-o +PCRE_A="$7" # //external-deps:pcre-build[lib] +DC_A="$8" # //external-deps:double-conversion-build +EXT_JS="$9" # //external-deps:compiler-js +LIB_JS="${10}" # //lib:tsjs (compiled+passed-through compiler JS) +HOST_CONFIG="${11}" # //lib:host-config.js +EJS_MAIN="${12}" # //lib:tsjs (again; driver at its root) +NC_EJS="${13}" # //node-compat:node-compat.ejs +NC_A="${14}" # //node-compat:node-compat[static] +LLVM_EJS="${15}" # //ejs-llvm:ejs-llvm.ejs +LLVM_A="${16}" # //ejs-llvm:ejs-llvm[static] +DTOA_A="${17}" # //runtime:echo-dtoa[static] +OBJC_A="${18}" # //runtime:echo-objc[static] on macos, "-" elsewhere + +mkdir -p "$OUT" +ROOT="$(cd "$OUT" && pwd)" + +# runtime headers + libecho.a: merge the runtime archives (C, C++, objc) +# and the llc'd trampoline object into the single archive the compiler +# links against, the way runtime/Makefile produces it. +mkdir -p "$ROOT/runtime/out/$TRIPLE" +cp -RL "$HDRS"/. "$ROOT/runtime/" +LIB="$ROOT/runtime/out/$TRIPLE/libecho.a" +cp "$ICC_O" "$TMP/ejs-invoke-closure-catch.o" +ARCHIVES=("$LIBECHO" "$DTOA_A") +if [ "$OBJC_A" != "-" ]; then + ARCHIVES+=("$OBJC_A") +fi +if [ "$OSNAME" = "macos" ]; then + libtool -static -o "$LIB" "${ARCHIVES[@]}" "$TMP/ejs-invoke-closure-catch.o" 2>/dev/null +else + MERGE="$TMP/libecho-merge" + rm -rf "$MERGE" + mkdir -p "$MERGE" + for a in "${ARCHIVES[@]}"; do + # absolutize BEFORE cd'ing: inside the subshell the relative + # archive path would resolve against $MERGE + abs="$(cd "$(dirname "$a")" && pwd)/$(basename "$a")" + (cd "$MERGE" && ar x "$abs") + done + ar rs "$LIB" "$MERGE"/*.o "$TMP/ejs-invoke-closure-catch.o" +fi +# some spots use the short triple for the runtime dir; provide both +mkdir -p "$ROOT/runtime/out/$SHORT_TRIPLE" +cp "$LIB" "$ROOT/runtime/out/$SHORT_TRIPLE/libecho.a" + +# external-deps: static libs where --srcdir mode expects them + JS modules +mkdir -p "$ROOT/external-deps/pcre-$OSNAME/.libs" +cp "$PCRE_A" "$ROOT/external-deps/pcre-$OSNAME/.libs/libpcre16.a" +mkdir -p "$ROOT/external-deps/double-conversion-$OSNAME/double-conversion" +cp "$DC_A" "$ROOT/external-deps/double-conversion-$OSNAME/double-conversion/libdouble-conversion.a" +cp -RL "$EXT_JS"/. "$ROOT/external-deps/" + +# compiler sources (the tsjs tree: tsc output + passed-through JS) +mkdir -p "$ROOT/lib" +cp -RL "$LIB_JS/lib"/. "$ROOT/lib/" +cp "$HOST_CONFIG" "$ROOT/lib/host-config.js" +cp "$EJS_MAIN/ejs-es6.js" "$ROOT/ejs-es6.js" + +# native modules +mkdir -p "$ROOT/node-compat" +cp "$NC_EJS" "$ROOT/node-compat/node-compat.ejs" +cp "$NC_A" "$ROOT/node-compat/libejsnodecompat-module.a" +mkdir -p "$ROOT/ejs-llvm" +cp "$LLVM_EJS" "$ROOT/ejs-llvm/ejs-llvm.ejs" +cp "$LLVM_A" "$ROOT/ejs-llvm/libejsllvm-module.a" diff --git a/buck-stage.sh b/buck-stage.sh new file mode 100644 index 00000000..75b10053 --- /dev/null +++ b/buck-stage.sh @@ -0,0 +1,63 @@ +#!/bin/bash +# Invoked by //:ejs.exe.stage{1,2,3}. Copies the --srcdir tree into a +# writable work dir and self-compiles ejs-es6.js in it, either with the +# node-hosted stage0 compiler or with the previous stage's executable. +set -euo pipefail + +TREE="$1" # //:srcdir-tree +MODE="$2" # "node" (stage0 compiler) or "exe" (previous stage binary) +COMPILER="$3" # node: //lib:generated dir; exe: previous ejs.exe.stageN +LLVM_NODE="$4" # node: //node-llvm:llvm.node; exe: "-" +LLVM_BIN="$5" # directory holding llc/opt (and llvm-config) +EXTRA_FLAGS="${6:-}" # extra compiler flags for the self-compile, e.g. --ir + +abspath() { + if [ -d "$1" ]; then + (cd "$1" && pwd) + else + echo "$(cd "$(dirname "$1")" && pwd)/$(basename "$1")" + fi +} + +OUT_ABS="$(cd "$(dirname "$OUT")" && pwd)/$(basename "$OUT")" +COMPILER_ABS="$(abspath "$COMPILER")" +if [ "$LLVM_NODE" != "-" ]; then + LLVM_NODE_ABS="$(abspath "$LLVM_NODE")" +fi + +WORK="$TMP/work" +rm -rf "$WORK" +mkdir -p "$WORK" +cp -RL "$TREE"/. "$WORK/" +chmod -R u+w "$WORK" + +cd "$WORK" + +# llc/opt for codegen; Apple clang++ for the final link on macOS so SDK +# discovery works. +export PATH="$LLVM_BIN:$PATH" +if [ "$(uname -s)" = "Darwin" ]; then + export CXX="${CXX:-/usr/bin/clang++}" + export SDKROOT="${SDKROOT:-$(/usr/bin/xcrun --show-sdk-path)}" +fi + +EJS_ARGS=(--srcdir --leave-temp --moduledir node-compat --moduledir ejs-llvm) +if [ -n "$EXTRA_FLAGS" ]; then + EJS_ARGS+=($EXTRA_FLAGS) +fi +EJS_ARGS+=(ejs-es6.js) + +if [ "$MODE" = "node" ]; then + mkdir -p lib/generated + cp -RL "$COMPILER_ABS"/. lib/generated/ + NODE_PATH="$(dirname "$LLVM_NODE_ABS")" \ + node lib/generated/ejs-es6.js "${EJS_ARGS[@]}" +else + cp "$COMPILER_ABS" ./ejs.exe.prev + chmod +x ./ejs.exe.prev + ./ejs.exe.prev "${EJS_ARGS[@]}" +fi + +test -f ejs-es6.js.exe +cp ejs-es6.js.exe "$OUT_ABS" +chmod +x "$OUT_ABS" diff --git a/buck-test-dist.sh b/buck-test-dist.sh new file mode 100644 index 00000000..720ff74b --- /dev/null +++ b/buck-test-dist.sh @@ -0,0 +1,107 @@ +#!/bin/bash +# Invoked by //:test-dist. Exercises the dist tarball the way an +# installer would: unpack it somewhere unrelated to the repo, compile +# and run programs with the installed layout (no --srcdir), and check +# that the LLVM version policy fails loudly when pointed at nothing. +set -euo pipefail + +DIST="$1" # //:dist (directory holding the tarball) +LLVM_BIN="$2" # build-config LLVM bindir; kept OFF PATH — discovery must + # find a toolchain on its own (baked path or conventional + # locations), which is exactly what an end user relies on + +# $OUT is genrule-relative; absolutize before cd'ing around +OUT="$(cd "$(dirname "$OUT")" && pwd)/$(basename "$OUT")" + +log() { echo "$@" | tee -a "$OUT"; } +: > "$OUT" + +WORK="$TMP/dist-test" +rm -rf "$WORK" +mkdir -p "$WORK" + +TARBALL="$(echo "$DIST"/echojs-*.tar.gz)" +test -f "$TARBALL" +tar -C "$WORK" -xzf "$TARBALL" +ROOT="$(echo "$WORK"/echojs-*)" +test -x "$ROOT/bin/ejs" +log "unpacked $(basename "$TARBALL")" + +# the final link wants a C++ driver; on macos use Apple clang++ so SDK +# discovery works (same as buck-stage.sh) +if [ "$(uname -s)" = "Darwin" ]; then + export CXX="${CXX:-/usr/bin/clang++}" + export SDKROOT="${SDKROOT:-$(/usr/bin/xcrun --show-sdk-path)}" +fi + +cd "$WORK" + +# 1: a plain program, no imports +cat > hello.js <<'EOF' +class Greeter { + constructor(who) { this.who = who; } + greet() { return `hello, ${this.who}`; } +} +let parts = ["from", "the", "installed", "echojs"].map((w) => w); +console.log(new Greeter(parts.join(" ")).greet()); +EOF +"$ROOT/bin/ejs" -q -o hello.exe hello.js >> "$OUT" 2>&1 +actual="$(./hello.exe)" +expected="hello, from the installed echojs" +if [ "$actual" != "$expected" ]; then + log "FAIL hello: got '$actual', want '$expected'" + exit 1 +fi +log "PASS hello" + +# 2: a program importing the node-compat native module (exercises the +# lib/ manifest scan + lib// module archive) +cat > pathtest.js <<'EOF' +import * as path from "@node-compat/path"; +console.log(path.basename(path.join("/a/b", "c.js"))); +EOF +"$ROOT/bin/ejs" -q -o pathtest.exe pathtest.js >> "$OUT" 2>&1 +actual="$(./pathtest.exe)" +if [ "$actual" != "c.js" ]; then + log "FAIL pathtest: got '$actual', want 'c.js'" + exit 1 +fi +log "PASS pathtest" + +# 3: the fail-loudly policy: pointed at a bindir with no LLVM, the +# driver must refuse (mentioning the required major), not miscompile +rm -f nollvm.exe +set +e +LLVM_BINDIR=/nonexistent "$ROOT/bin/ejs" -q -o nollvm.exe hello.js > nollvm.log 2>&1 +status=$? +set -e +cat nollvm.log >> "$OUT" +if [ "$status" -eq 0 ] || [ -f nollvm.exe ]; then + log "FAIL nollvm: expected a loud failure, got exit $status" + exit 1 +fi +if ! grep -q "requires LLVM" nollvm.log; then + log "FAIL nollvm: no version-policy message in the failure output" + exit 1 +fi +log "PASS nollvm (exit $status)" + +# 4: the bundled prefix installer (release-P2): install into a scratch +# prefix, compile through the bin/ejs shim, uninstall, assert it's gone +PREFIX="$WORK/prefix" +"$ROOT/install.sh" --prefix "$PREFIX" >> "$OUT" 2>&1 +test -x "$PREFIX/bin/ejs" +"$PREFIX/bin/ejs" -q -o hello-installed.exe hello.js >> "$OUT" 2>&1 +actual="$(./hello-installed.exe)" +if [ "$actual" != "$expected" ]; then + log "FAIL install.sh: got '$actual', want '$expected'" + exit 1 +fi +"$ROOT/install.sh" --prefix "$PREFIX" --uninstall >> "$OUT" 2>&1 +if [ -e "$PREFIX/bin/ejs" ] || [ -e "$PREFIX/lib/echojs" ]; then + log "FAIL install.sh: uninstall left files behind" + exit 1 +fi +log "PASS install.sh" + +log "test-dist OK" diff --git a/buck-test-lowtier.sh b/buck-test-lowtier.sh new file mode 100644 index 00000000..27fffee4 --- /dev/null +++ b/buck-test-lowtier.sh @@ -0,0 +1,90 @@ +#!/bin/bash +# Invoked by //:test-eir-lowtier. Compiles test/eir-lowtier1.js twice with +# the stage0 (node-hosted) compiler — once plain, once with -flowtier +# (which swaps the lowtier_* function bodies for hand-built low-tier EIR, +# see lib/eir/lowtier-probe.ts) — runs both executables, and fails unless: +# - both outputs match the committed expected-out byte for byte; +# - the injected build actually differs from the plain one; and +# - the injected build's LLVM IR contains every low-tier float op +# (fadd/fsub/fmul/fdiv/fcmp olt) — so a silent injection no-op or a +# stale prebuilt llvm.node missing the FP bindings fails loudly. +set -euo pipefail + +TREE="$1" # //:srcdir-tree +GENERATED="$2" # //lib:generated +TEST_FILES="$3" # //test:files +LLVM_BIN="$4" # directory holding llc/opt + +REPO="${TMP%%/buck-out/*}" +OUT_ABS="$(cd "$(dirname "$OUT")" && pwd)/$(basename "$OUT")" + +WORK="$TMP/lowtier" +rm -rf "$WORK" +mkdir -p "$WORK" +cp -RL "$TREE"/. "$WORK/" +chmod -R u+w "$WORK" +mkdir -p "$WORK/lib/generated" +cp -RL "$GENERATED"/. "$WORK/lib/generated/" +mkdir -p "$WORK/test" +cp -RL "$TEST_FILES"/. "$WORK/test/" +chmod -R u+w "$WORK/test" + +export PATH="$LLVM_BIN:$PATH" +export NODE_PATH="$REPO/node_modules:$REPO/node-llvm/build/Release" +if [ "$(uname -s)" = "Darwin" ]; then + export SDKROOT="${SDKROOT:-$(/usr/bin/xcrun --show-sdk-path)}" +fi + +cd "$WORK/test" +EXPECTED=expected/eir-lowtier1.js.expected-out +EJS_ARGS=(--srcdir --moduledir ../node-compat --moduledir ../ejs-llvm) + +# NOTE: `run` is invoked in an `if` condition, which disables `set -e` +# inside it (the classic bash trap — an early version of this script +# printed OK over an aborting executable). Every step therefore checks +# its own status explicitly. +run() { + echo "== plain build ==" + node ../lib/generated/ejs-es6.js "${EJS_ARGS[@]}" eir-lowtier1.js \ + || { echo "ERROR: plain compile failed"; return 1; } + ./eir-lowtier1.js.exe > plain.out \ + || { echo "ERROR: plain executable failed"; return 1; } + diff -u "$EXPECTED" plain.out \ + || { echo "ERROR: plain output does not match expected"; return 1; } + cp eir-lowtier1.js.exe plain.exe + + echo "== injected build ==" + mkdir -p "$WORK/ltmp" + TMPDIR="$WORK/ltmp" \ + node ../lib/generated/ejs-es6.js "${EJS_ARGS[@]}" --leave-temp -flowtier eir-lowtier1.js \ + || { echo "ERROR: injected compile failed"; return 1; } + ./eir-lowtier1.js.exe > injected.out \ + || { echo "ERROR: injected executable failed"; return 1; } + diff -u "$EXPECTED" injected.out \ + || { echo "ERROR: injected output does not match expected"; return 1; } + + if cmp -s plain.exe eir-lowtier1.js.exe; then + echo "ERROR: injected binary is identical to the plain build (injection no-op?)" + return 1 + fi + + LL=$(ls "$WORK"/ltmp/eir-lowtier1.js.*.ll 2>/dev/null | head -1) + if [ -z "$LL" ]; then + echo "ERROR: no --leave-temp .ll found under $WORK/ltmp" + return 1 + fi + for pat in "fadd double" "fsub double" "fmul double" "fdiv double" "fcmp olt double"; do + if ! grep -q "$pat" "$LL"; then + echo "ERROR: '$pat' missing from $LL — the low tier was not emitted" + return 1 + fi + done + echo "lowtier e2e OK: outputs match expected, binaries differ, all f64 ops in the IR" +} + +if run > "$OUT_ABS" 2>&1; then + tail -1 "$OUT_ABS" +else + cat "$OUT_ABS" >&2 + exit 1 +fi diff --git a/buck-test-stage.sh b/buck-test-stage.sh new file mode 100644 index 00000000..0b710d40 --- /dev/null +++ b/buck-test-stage.sh @@ -0,0 +1,79 @@ +#!/bin/bash +# Invoked by //:test-stage{1,2,3}. Assembles a repo-shaped tree (the +# --srcdir tree + test/ + the stage executable), compiles the runner +# (test/tester.ts) in the staged tree, and runs it. The genrule fails +# if any test fails; the test log is the output artifact. +set -euo pipefail + +TREE="$1" # //:srcdir-tree +GENERATED="$2" # //lib:generated (tester requires ../lib/generated/.../host-config.js) +STAGE_EXE="$3" # //:ejs.exe.stageN, or "-" for stage 0 (node-hosted) +STAGE_NUM="$4" # N +TEST_FILES="$5" # //test:files +LLVM_BIN="$6" # directory holding llc/opt +EXTRA_FLAGS="${7:-}" # extra compiler flags, e.g. --ir +TEST_ENV="${8:-}" # extra env for the tester run, e.g. EJS_SHAPES=off + # (the runtime A/B lanes: shapes-plan P4.1) + +# node_modules (glob/colors/temp for the tester, typescript for the +# tester compile + esm baseline generation) come from the repo, same as +# the tsc steps in //lib. +REPO="${TMP%%/buck-out/*}" + +OUT_ABS="$(cd "$(dirname "$OUT")" && pwd)/$(basename "$OUT")" + +WORK="$TMP/testroot" +rm -rf "$WORK" +mkdir -p "$WORK" +cp -RL "$TREE"/. "$WORK/" +chmod -R u+w "$WORK" +mkdir -p "$WORK/lib/generated" +cp -RL "$GENERATED"/. "$WORK/lib/generated/" +if [ "$STAGE_NUM" = "0" ]; then + # stage 0 runs the generated (CommonJS) compiler under node via the + # ../ejs driver + printf '#!/bin/sh\ndir=$(cd `dirname $0`; pwd)\nexec node $dir/lib/generated/ejs-es6.js "$@"\n' > "$WORK/ejs" + chmod +x "$WORK/ejs" +else + cp "$STAGE_EXE" "$WORK/ejs.exe.stage$STAGE_NUM" + chmod +x "$WORK/ejs.exe.stage$STAGE_NUM" +fi +mkdir -p "$WORK/test" +cp -RL "$TEST_FILES"/. "$WORK/test/" +chmod -R u+w "$WORK/test" + +# the runner is TypeScript (compiler-P2): compile the staged copy in +# place — tsconfig.json ships with the test tree +node "$REPO/node_modules/typescript/bin/tsc" -p "$WORK/test" + +# the tester regenerates an expected-out (using node) when the test file +# is newer than it; the copies above have fresh mtimes, so re-stamp the +# expected outputs afterwards to keep them newer. +find "$WORK/test" -name '*.js' -exec touch {} + +find "$WORK/test/expected" -type f -exec touch {} + + +export PATH="$LLVM_BIN:$PATH" +export NODE_PATH="$REPO/node_modules:$REPO/node-llvm/build/Release" +# the tester regenerates missing expected-outs by RUNNING node: keep that +# color-free even when the buck daemon inherited a colored dev shell +# (FORCE_COLOR writes ANSI into the expected files and poisons the diffs) +export NO_COLOR=1 +unset FORCE_COLOR +if [ -n "$EXTRA_FLAGS" ]; then + export EJS_EXTRA_FLAGS="$EXTRA_FLAGS" +fi +if [ -n "$TEST_ENV" ]; then + export $TEST_ENV +fi +if [ "$(uname -s)" = "Darwin" ]; then + export SDKROOT="${SDKROOT:-$(/usr/bin/xcrun --show-sdk-path)}" +fi + +cd "$WORK/test" +if node tester.js -s "$STAGE_NUM" > "$OUT_ABS" 2>&1; then + tail -5 "$OUT_ABS" +else + echo "stage$STAGE_NUM tests FAILED:" >&2 + tail -40 "$OUT_ABS" >&2 + exit 1 +fi diff --git a/buck-test-types-diff.sh b/buck-test-types-diff.sh new file mode 100755 index 00000000..02efb20e --- /dev/null +++ b/buck-test-types-diff.sh @@ -0,0 +1,144 @@ +#!/bin/bash +# The Phase 3 --types diff lane (docs/maam-plan.md, P3 gate): compile every +# test/*.js twice with the node-hosted compiler — flag-off and --types — run +# both executables, and byte-compare RUN STDOUT. Any divergence is a Phase 3 +# stop-the-line bug: --types may only change code size/speed, never behavior. +# +# Protocol (lessons from the measurement chunks baked in): +# - stdout only: --types adds stats lines to stderr by design, and the +# debug runtime traces normally-handled EXCEPTIONS to stderr; +# - color-free: node inherits FORCE_COLOR from dev shells (and a buck +# daemon started from one) — everything runs under NO_COLOR with +# FORCE_COLOR stripped; +# - files that fail to compile flag-off are N/A (tester.js — an esprima +# parse gap — is the standing one), not lane failures; +# - per-file timeout discipline (120 s, kill and record); +# - the maam CJS dist must be built (external-deps/echojs-maam: +# `npm run build && npm run build:cjs`). +# +# Standalone by design (not genrule-wired: a ~15-minute double compile of +# the whole suite is a CI-lane decision, not a default build step). Run +# from the repo root after `buck2 build //lib:generated //:srcdir-tree`: +# +# ./buck-test-types-diff.sh [concurrency] +# +# where is a stage0-style tree (srcdir-tree + lib/generated + +# test/) — the caller assembles it so this script never mixes trees +# (franken-tree lesson). Writes per-file logs + results.jsonl to +# and prints the summary table. +set -euo pipefail + +# Absolutize both paths up front: a relative once resolved +# against each worker's cwd, turning every compile into an N/A and the +# lane into a 100%-N/A exit-0 "PASS" (review finding M1). The tree must +# also live INSIDE the repo checkout so the probe can find +# external-deps/echojs-maam — from /tmp the oracle silently skips and the +# lane tests nothing Phase-3-specific (guarded below by diamonds==0). +WORK="$(cd "$1" && pwd)" +mkdir -p "$2" +LOGDIR="$(cd "$2" && pwd)" +CONC="${3:-4}" + +export NODE_PATH="/Users/toshok/src/echojs/echojs/node_modules:/Users/toshok/src/echojs/echojs/node-llvm/build/Release" +export PATH="/opt/homebrew/opt/llvm/bin:$PATH" +if [ "$(uname -s)" = "Darwin" ]; then + export SDKROOT="${SDKROOT:-$(/usr/bin/xcrun --show-sdk-path)}" +fi +export NO_COLOR=1 +unset FORCE_COLOR + +WORK="$WORK" LOGDIR="$LOGDIR" CONC="$CONC" exec node --input-type=module -e ' +import { spawn } from "node:child_process"; +import * as fs from "node:fs"; +import * as path from "node:path"; + +const WORK = process.env.WORK; +const LOGDIR = process.env.LOGDIR; +const CONC = Number(process.env.CONC || 4); +const TIMEOUT_MS = 120000; +const testDir = path.join(WORK, "test"); + +const files = fs.readdirSync(testDir).filter((f) => f.endsWith(".js") && !f.includes("/")).sort(); + +function run(cmd, args, opts, timeoutMs) { + return new Promise((resolve) => { + const child = spawn(cmd, args, { ...opts, stdio: ["ignore", "pipe", "pipe"] }); + let out = Buffer.alloc(0), err = Buffer.alloc(0), timedout = false, failed = null; + child.stdout.on("data", (d) => (out = Buffer.concat([out, d]))); + child.stderr.on("data", (d) => (err = Buffer.concat([err, d]))); + const t = setTimeout(() => { timedout = true; child.kill("SIGKILL"); }, timeoutMs); + // a spawn failure (ENOENT — e.g. an exe that never materialized) + // must be a recorded per-file anomaly, never a lane crash + child.on("error", (e) => { clearTimeout(t); failed = String(e); resolve({ code: -1, out, err, timedout, failed }); }); + child.on("exit", (code) => { clearTimeout(t); resolve({ code, out, err, timedout, failed }); }); + }); +} + +const EJS = ["--srcdir", "--moduledir", "../node-compat", "--moduledir", "../ejs-llvm"]; +const results = []; +let idx = 0; + +async function worker(wid) { + // per-worker TMPDIR: concurrent compiles never share temp space + const tmp = path.join(LOGDIR, "tmp" + wid); + fs.mkdirSync(tmp, { recursive: true }); + const env = { ...process.env, TMPDIR: tmp }; + for (;;) { + const file = files[idx++]; + if (!file) return; + const base = file.replace(/\.js$/, ""); + const exe = path.join(testDir, file + ".exe"); + const r = { file, status: "?", diamonds: 0, queries: 0, unknown: 0 }; + + // flag-off compile + run + const c0 = await run("node", [path.join(WORK, "lib/generated/ejs-es6.js"), ...EJS, file], { cwd: testDir, env }, TIMEOUT_MS); + if (c0.timedout) { r.status = "TIMEOUT-compile-off"; results.push(r); continue; } + if (c0.code !== 0) { r.status = "N/A"; results.push(r); continue; } + const off = await run(exe, [], { cwd: testDir, env }, TIMEOUT_MS); + if (off.failed) { r.status = "RUN-OFF-SPAWN-FAIL"; results.push(r); continue; } + if (off.timedout) { r.status = "TIMEOUT-run-off"; results.push(r); continue; } + + // --types compile + run + const c1 = await run("node", [path.join(WORK, "lib/generated/ejs-es6.js"), ...EJS, "--types", file], { cwd: testDir, env }, TIMEOUT_MS); + if (c1.timedout) { r.status = "TIMEOUT-compile-on"; results.push(r); continue; } + if (c1.code !== 0) { r.status = "TYPES-COMPILE-FAIL"; results.push(r); continue; } + fs.writeFileSync(path.join(LOGDIR, base + ".types.err"), c1.err); + const m = String(c1.err).match(/diamonds=(\d+) oracleQueries=(\d+) oracleUnknown=(\d+)/g) || []; + for (const line of m) { + const g = line.match(/diamonds=(\d+) oracleQueries=(\d+) oracleUnknown=(\d+)/); + r.diamonds += +g[1]; r.queries += +g[2]; r.unknown += +g[3]; + } + const on = await run(exe, [], { cwd: testDir, env }, TIMEOUT_MS); + if (on.failed) { r.status = "RUN-ON-SPAWN-FAIL"; results.push(r); continue; } + if (on.timedout) { r.status = "TIMEOUT-run-on"; results.push(r); continue; } + + if (Buffer.compare(off.out, on.out) === 0 && off.code === on.code) { + r.status = "IDENTICAL"; + } else { + r.status = "DIVERGENT"; + fs.writeFileSync(path.join(LOGDIR, base + ".off.out"), off.out); + fs.writeFileSync(path.join(LOGDIR, base + ".on.out"), on.out); + } + results.push(r); + process.stdout.write(`${file} ${r.status} diamonds=${r.diamonds}\n`); + } +} + +await Promise.all(Array.from({ length: CONC }, (_, i) => worker(i))); + +fs.writeFileSync(path.join(LOGDIR, "results.jsonl"), results.map((r) => JSON.stringify(r)).join("\n") + "\n"); +const by = (s) => results.filter((r) => r.status === s); +const identical = by("IDENTICAL"), divergent = by("DIVERGENT"), na = by("N/A"); +const other = results.filter((r) => !["IDENTICAL", "DIVERGENT", "N/A"].includes(r.status)); +const tot = (k) => results.reduce((a, r) => a + r[k], 0); +console.log("==== --types diff lane summary ===="); +console.log(`files: ${results.length} identical: ${identical.length} divergent: ${divergent.length} N/A: ${na.length} other: ${other.length}`); +console.log(`diamonds total: ${tot("diamonds")} oracleQueries: ${tot("queries")} oracleUnknown: ${tot("unknown")}`); +if (divergent.length) { console.log("DIVERGENT:", divergent.map((r) => r.file).join(" ")); process.exit(1); } +if (other.length) { console.log("OTHER:", other.map((r) => `${r.file}:${r.status}`).join(" ")); process.exit(1); } +// Vacuous-pass guards (review findings M1/M2): a lane that compared zero +// files, or ran with no live oracle (no diamonds anywhere), proves nothing. +if (identical.length === 0) { console.log("LANE FAIL: zero files compared (all N/A) — bad work tree?"); process.exit(1); } +if (tot("diamonds") === 0) { console.log("LANE FAIL: diamonds total is 0 — no live oracle (work tree outside the repo checkout, or maam dist unbuilt); the lane tested nothing Phase-3-specific"); process.exit(1); } +console.log("LANE PASS: zero divergence"); +' diff --git a/build/.gitignore b/build/.gitignore deleted file mode 100644 index a0c1e801..00000000 --- a/build/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -config-local.mk -host-config.mk diff --git a/build/build.mk b/build/build.mk deleted file mode 100644 index 1f72364e..00000000 --- a/build/build.mk +++ /dev/null @@ -1,2 +0,0 @@ -include $(TOP)/build/utils.mk -include $(TOP)/build/rules.mk diff --git a/build/config.guess b/build/config.guess deleted file mode 100755 index b79252d6..00000000 --- a/build/config.guess +++ /dev/null @@ -1,1558 +0,0 @@ -#! /bin/sh -# Attempt to guess a canonical system name. -# Copyright 1992-2013 Free Software Foundation, Inc. - -timestamp='2013-06-10' - -# This file is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, see . -# -# As a special exception to the GNU General Public License, if you -# distribute this file as part of a program that contains a -# configuration script generated by Autoconf, you may include it under -# the same distribution terms that you use for the rest of that -# program. This Exception is an additional permission under section 7 -# of the GNU General Public License, version 3 ("GPLv3"). -# -# Originally written by Per Bothner. -# -# You can get the latest version of this script from: -# http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.guess;hb=HEAD -# -# Please send patches with a ChangeLog entry to config-patches@gnu.org. - - -me=`echo "$0" | sed -e 's,.*/,,'` - -usage="\ -Usage: $0 [OPTION] - -Output the configuration name of the system \`$me' is run on. - -Operation modes: - -h, --help print this help, then exit - -t, --time-stamp print date of last modification, then exit - -v, --version print version number, then exit - -Report bugs and patches to ." - -version="\ -GNU config.guess ($timestamp) - -Originally written by Per Bothner. -Copyright 1992-2013 Free Software Foundation, Inc. - -This is free software; see the source for copying conditions. There is NO -warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE." - -help=" -Try \`$me --help' for more information." - -# Parse command line -while test $# -gt 0 ; do - case $1 in - --time-stamp | --time* | -t ) - echo "$timestamp" ; exit ;; - --version | -v ) - echo "$version" ; exit ;; - --help | --h* | -h ) - echo "$usage"; exit ;; - -- ) # Stop option processing - shift; break ;; - - ) # Use stdin as input. - break ;; - -* ) - echo "$me: invalid option $1$help" >&2 - exit 1 ;; - * ) - break ;; - esac -done - -if test $# != 0; then - echo "$me: too many arguments$help" >&2 - exit 1 -fi - -trap 'exit 1' 1 2 15 - -# CC_FOR_BUILD -- compiler used by this script. Note that the use of a -# compiler to aid in system detection is discouraged as it requires -# temporary files to be created and, as you can see below, it is a -# headache to deal with in a portable fashion. - -# Historically, `CC_FOR_BUILD' used to be named `HOST_CC'. We still -# use `HOST_CC' if defined, but it is deprecated. - -# Portable tmp directory creation inspired by the Autoconf team. - -set_cc_for_build=' -trap "exitcode=\$?; (rm -f \$tmpfiles 2>/dev/null; rmdir \$tmp 2>/dev/null) && exit \$exitcode" 0 ; -trap "rm -f \$tmpfiles 2>/dev/null; rmdir \$tmp 2>/dev/null; exit 1" 1 2 13 15 ; -: ${TMPDIR=/tmp} ; - { tmp=`(umask 077 && mktemp -d "$TMPDIR/cgXXXXXX") 2>/dev/null` && test -n "$tmp" && test -d "$tmp" ; } || - { test -n "$RANDOM" && tmp=$TMPDIR/cg$$-$RANDOM && (umask 077 && mkdir $tmp) ; } || - { tmp=$TMPDIR/cg-$$ && (umask 077 && mkdir $tmp) && echo "Warning: creating insecure temp directory" >&2 ; } || - { echo "$me: cannot create a temporary directory in $TMPDIR" >&2 ; exit 1 ; } ; -dummy=$tmp/dummy ; -tmpfiles="$dummy.c $dummy.o $dummy.rel $dummy" ; -case $CC_FOR_BUILD,$HOST_CC,$CC in - ,,) echo "int x;" > $dummy.c ; - for c in cc gcc c89 c99 ; do - if ($c -c -o $dummy.o $dummy.c) >/dev/null 2>&1 ; then - CC_FOR_BUILD="$c"; break ; - fi ; - done ; - if test x"$CC_FOR_BUILD" = x ; then - CC_FOR_BUILD=no_compiler_found ; - fi - ;; - ,,*) CC_FOR_BUILD=$CC ;; - ,*,*) CC_FOR_BUILD=$HOST_CC ;; -esac ; set_cc_for_build= ;' - -# This is needed to find uname on a Pyramid OSx when run in the BSD universe. -# (ghazi@noc.rutgers.edu 1994-08-24) -if (test -f /.attbin/uname) >/dev/null 2>&1 ; then - PATH=$PATH:/.attbin ; export PATH -fi - -UNAME_MACHINE=`(uname -m) 2>/dev/null` || UNAME_MACHINE=unknown -UNAME_RELEASE=`(uname -r) 2>/dev/null` || UNAME_RELEASE=unknown -UNAME_SYSTEM=`(uname -s) 2>/dev/null` || UNAME_SYSTEM=unknown -UNAME_VERSION=`(uname -v) 2>/dev/null` || UNAME_VERSION=unknown - -case "${UNAME_SYSTEM}" in -Linux|GNU|GNU/*) - # If the system lacks a compiler, then just pick glibc. - # We could probably try harder. - LIBC=gnu - - eval $set_cc_for_build - cat <<-EOF > $dummy.c - #include - #if defined(__UCLIBC__) - LIBC=uclibc - #elif defined(__dietlibc__) - LIBC=dietlibc - #else - LIBC=gnu - #endif - EOF - eval `$CC_FOR_BUILD -E $dummy.c 2>/dev/null | grep '^LIBC'` - ;; -esac - -# Note: order is significant - the case branches are not exclusive. - -case "${UNAME_MACHINE}:${UNAME_SYSTEM}:${UNAME_RELEASE}:${UNAME_VERSION}" in - *:NetBSD:*:*) - # NetBSD (nbsd) targets should (where applicable) match one or - # more of the tuples: *-*-netbsdelf*, *-*-netbsdaout*, - # *-*-netbsdecoff* and *-*-netbsd*. For targets that recently - # switched to ELF, *-*-netbsd* would select the old - # object file format. This provides both forward - # compatibility and a consistent mechanism for selecting the - # object file format. - # - # Note: NetBSD doesn't particularly care about the vendor - # portion of the name. We always set it to "unknown". - sysctl="sysctl -n hw.machine_arch" - UNAME_MACHINE_ARCH=`(/sbin/$sysctl 2>/dev/null || \ - /usr/sbin/$sysctl 2>/dev/null || echo unknown)` - case "${UNAME_MACHINE_ARCH}" in - armeb) machine=armeb-unknown ;; - arm*) machine=arm-unknown ;; - sh3el) machine=shl-unknown ;; - sh3eb) machine=sh-unknown ;; - sh5el) machine=sh5le-unknown ;; - *) machine=${UNAME_MACHINE_ARCH}-unknown ;; - esac - # The Operating System including object format, if it has switched - # to ELF recently, or will in the future. - case "${UNAME_MACHINE_ARCH}" in - arm*|i386|m68k|ns32k|sh3*|sparc|vax) - eval $set_cc_for_build - if echo __ELF__ | $CC_FOR_BUILD -E - 2>/dev/null \ - | grep -q __ELF__ - then - # Once all utilities can be ECOFF (netbsdecoff) or a.out (netbsdaout). - # Return netbsd for either. FIX? - os=netbsd - else - os=netbsdelf - fi - ;; - *) - os=netbsd - ;; - esac - # The OS release - # Debian GNU/NetBSD machines have a different userland, and - # thus, need a distinct triplet. However, they do not need - # kernel version information, so it can be replaced with a - # suitable tag, in the style of linux-gnu. - case "${UNAME_VERSION}" in - Debian*) - release='-gnu' - ;; - *) - release=`echo ${UNAME_RELEASE}|sed -e 's/[-_].*/\./'` - ;; - esac - # Since CPU_TYPE-MANUFACTURER-KERNEL-OPERATING_SYSTEM: - # contains redundant information, the shorter form: - # CPU_TYPE-MANUFACTURER-OPERATING_SYSTEM is used. - echo "${machine}-${os}${release}" - exit ;; - *:Bitrig:*:*) - UNAME_MACHINE_ARCH=`arch | sed 's/Bitrig.//'` - echo ${UNAME_MACHINE_ARCH}-unknown-bitrig${UNAME_RELEASE} - exit ;; - *:OpenBSD:*:*) - UNAME_MACHINE_ARCH=`arch | sed 's/OpenBSD.//'` - echo ${UNAME_MACHINE_ARCH}-unknown-openbsd${UNAME_RELEASE} - exit ;; - *:ekkoBSD:*:*) - echo ${UNAME_MACHINE}-unknown-ekkobsd${UNAME_RELEASE} - exit ;; - *:SolidBSD:*:*) - echo ${UNAME_MACHINE}-unknown-solidbsd${UNAME_RELEASE} - exit ;; - macppc:MirBSD:*:*) - echo powerpc-unknown-mirbsd${UNAME_RELEASE} - exit ;; - *:MirBSD:*:*) - echo ${UNAME_MACHINE}-unknown-mirbsd${UNAME_RELEASE} - exit ;; - alpha:OSF1:*:*) - case $UNAME_RELEASE in - *4.0) - UNAME_RELEASE=`/usr/sbin/sizer -v | awk '{print $3}'` - ;; - *5.*) - UNAME_RELEASE=`/usr/sbin/sizer -v | awk '{print $4}'` - ;; - esac - # According to Compaq, /usr/sbin/psrinfo has been available on - # OSF/1 and Tru64 systems produced since 1995. I hope that - # covers most systems running today. This code pipes the CPU - # types through head -n 1, so we only detect the type of CPU 0. - ALPHA_CPU_TYPE=`/usr/sbin/psrinfo -v | sed -n -e 's/^ The alpha \(.*\) processor.*$/\1/p' | head -n 1` - case "$ALPHA_CPU_TYPE" in - "EV4 (21064)") - UNAME_MACHINE="alpha" ;; - "EV4.5 (21064)") - UNAME_MACHINE="alpha" ;; - "LCA4 (21066/21068)") - UNAME_MACHINE="alpha" ;; - "EV5 (21164)") - UNAME_MACHINE="alphaev5" ;; - "EV5.6 (21164A)") - UNAME_MACHINE="alphaev56" ;; - "EV5.6 (21164PC)") - UNAME_MACHINE="alphapca56" ;; - "EV5.7 (21164PC)") - UNAME_MACHINE="alphapca57" ;; - "EV6 (21264)") - UNAME_MACHINE="alphaev6" ;; - "EV6.7 (21264A)") - UNAME_MACHINE="alphaev67" ;; - "EV6.8CB (21264C)") - UNAME_MACHINE="alphaev68" ;; - "EV6.8AL (21264B)") - UNAME_MACHINE="alphaev68" ;; - "EV6.8CX (21264D)") - UNAME_MACHINE="alphaev68" ;; - "EV6.9A (21264/EV69A)") - UNAME_MACHINE="alphaev69" ;; - "EV7 (21364)") - UNAME_MACHINE="alphaev7" ;; - "EV7.9 (21364A)") - UNAME_MACHINE="alphaev79" ;; - esac - # A Pn.n version is a patched version. - # A Vn.n version is a released version. - # A Tn.n version is a released field test version. - # A Xn.n version is an unreleased experimental baselevel. - # 1.2 uses "1.2" for uname -r. - echo ${UNAME_MACHINE}-dec-osf`echo ${UNAME_RELEASE} | sed -e 's/^[PVTX]//' | tr 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz'` - # Reset EXIT trap before exiting to avoid spurious non-zero exit code. - exitcode=$? - trap '' 0 - exit $exitcode ;; - Alpha\ *:Windows_NT*:*) - # How do we know it's Interix rather than the generic POSIX subsystem? - # Should we change UNAME_MACHINE based on the output of uname instead - # of the specific Alpha model? - echo alpha-pc-interix - exit ;; - 21064:Windows_NT:50:3) - echo alpha-dec-winnt3.5 - exit ;; - Amiga*:UNIX_System_V:4.0:*) - echo m68k-unknown-sysv4 - exit ;; - *:[Aa]miga[Oo][Ss]:*:*) - echo ${UNAME_MACHINE}-unknown-amigaos - exit ;; - *:[Mm]orph[Oo][Ss]:*:*) - echo ${UNAME_MACHINE}-unknown-morphos - exit ;; - *:OS/390:*:*) - echo i370-ibm-openedition - exit ;; - *:z/VM:*:*) - echo s390-ibm-zvmoe - exit ;; - *:OS400:*:*) - echo powerpc-ibm-os400 - exit ;; - arm:RISC*:1.[012]*:*|arm:riscix:1.[012]*:*) - echo arm-acorn-riscix${UNAME_RELEASE} - exit ;; - arm*:riscos:*:*|arm*:RISCOS:*:*) - echo arm-unknown-riscos - exit ;; - SR2?01:HI-UX/MPP:*:* | SR8000:HI-UX/MPP:*:*) - echo hppa1.1-hitachi-hiuxmpp - exit ;; - Pyramid*:OSx*:*:* | MIS*:OSx*:*:* | MIS*:SMP_DC-OSx*:*:*) - # akee@wpdis03.wpafb.af.mil (Earle F. Ake) contributed MIS and NILE. - if test "`(/bin/universe) 2>/dev/null`" = att ; then - echo pyramid-pyramid-sysv3 - else - echo pyramid-pyramid-bsd - fi - exit ;; - NILE*:*:*:dcosx) - echo pyramid-pyramid-svr4 - exit ;; - DRS?6000:unix:4.0:6*) - echo sparc-icl-nx6 - exit ;; - DRS?6000:UNIX_SV:4.2*:7* | DRS?6000:isis:4.2*:7*) - case `/usr/bin/uname -p` in - sparc) echo sparc-icl-nx7; exit ;; - esac ;; - s390x:SunOS:*:*) - echo ${UNAME_MACHINE}-ibm-solaris2`echo ${UNAME_RELEASE}|sed -e 's/[^.]*//'` - exit ;; - sun4H:SunOS:5.*:*) - echo sparc-hal-solaris2`echo ${UNAME_RELEASE}|sed -e 's/[^.]*//'` - exit ;; - sun4*:SunOS:5.*:* | tadpole*:SunOS:5.*:*) - echo sparc-sun-solaris2`echo ${UNAME_RELEASE}|sed -e 's/[^.]*//'` - exit ;; - i86pc:AuroraUX:5.*:* | i86xen:AuroraUX:5.*:*) - echo i386-pc-auroraux${UNAME_RELEASE} - exit ;; - i86pc:SunOS:5.*:* | i86xen:SunOS:5.*:*) - eval $set_cc_for_build - SUN_ARCH="i386" - # If there is a compiler, see if it is configured for 64-bit objects. - # Note that the Sun cc does not turn __LP64__ into 1 like gcc does. - # This test works for both compilers. - if [ "$CC_FOR_BUILD" != 'no_compiler_found' ]; then - if (echo '#ifdef __amd64'; echo IS_64BIT_ARCH; echo '#endif') | \ - (CCOPTS= $CC_FOR_BUILD -E - 2>/dev/null) | \ - grep IS_64BIT_ARCH >/dev/null - then - SUN_ARCH="x86_64" - fi - fi - echo ${SUN_ARCH}-pc-solaris2`echo ${UNAME_RELEASE}|sed -e 's/[^.]*//'` - exit ;; - sun4*:SunOS:6*:*) - # According to config.sub, this is the proper way to canonicalize - # SunOS6. Hard to guess exactly what SunOS6 will be like, but - # it's likely to be more like Solaris than SunOS4. - echo sparc-sun-solaris3`echo ${UNAME_RELEASE}|sed -e 's/[^.]*//'` - exit ;; - sun4*:SunOS:*:*) - case "`/usr/bin/arch -k`" in - Series*|S4*) - UNAME_RELEASE=`uname -v` - ;; - esac - # Japanese Language versions have a version number like `4.1.3-JL'. - echo sparc-sun-sunos`echo ${UNAME_RELEASE}|sed -e 's/-/_/'` - exit ;; - sun3*:SunOS:*:*) - echo m68k-sun-sunos${UNAME_RELEASE} - exit ;; - sun*:*:4.2BSD:*) - UNAME_RELEASE=`(sed 1q /etc/motd | awk '{print substr($5,1,3)}') 2>/dev/null` - test "x${UNAME_RELEASE}" = "x" && UNAME_RELEASE=3 - case "`/bin/arch`" in - sun3) - echo m68k-sun-sunos${UNAME_RELEASE} - ;; - sun4) - echo sparc-sun-sunos${UNAME_RELEASE} - ;; - esac - exit ;; - aushp:SunOS:*:*) - echo sparc-auspex-sunos${UNAME_RELEASE} - exit ;; - # The situation for MiNT is a little confusing. The machine name - # can be virtually everything (everything which is not - # "atarist" or "atariste" at least should have a processor - # > m68000). The system name ranges from "MiNT" over "FreeMiNT" - # to the lowercase version "mint" (or "freemint"). Finally - # the system name "TOS" denotes a system which is actually not - # MiNT. But MiNT is downward compatible to TOS, so this should - # be no problem. - atarist[e]:*MiNT:*:* | atarist[e]:*mint:*:* | atarist[e]:*TOS:*:*) - echo m68k-atari-mint${UNAME_RELEASE} - exit ;; - atari*:*MiNT:*:* | atari*:*mint:*:* | atarist[e]:*TOS:*:*) - echo m68k-atari-mint${UNAME_RELEASE} - exit ;; - *falcon*:*MiNT:*:* | *falcon*:*mint:*:* | *falcon*:*TOS:*:*) - echo m68k-atari-mint${UNAME_RELEASE} - exit ;; - milan*:*MiNT:*:* | milan*:*mint:*:* | *milan*:*TOS:*:*) - echo m68k-milan-mint${UNAME_RELEASE} - exit ;; - hades*:*MiNT:*:* | hades*:*mint:*:* | *hades*:*TOS:*:*) - echo m68k-hades-mint${UNAME_RELEASE} - exit ;; - *:*MiNT:*:* | *:*mint:*:* | *:*TOS:*:*) - echo m68k-unknown-mint${UNAME_RELEASE} - exit ;; - m68k:machten:*:*) - echo m68k-apple-machten${UNAME_RELEASE} - exit ;; - powerpc:machten:*:*) - echo powerpc-apple-machten${UNAME_RELEASE} - exit ;; - RISC*:Mach:*:*) - echo mips-dec-mach_bsd4.3 - exit ;; - RISC*:ULTRIX:*:*) - echo mips-dec-ultrix${UNAME_RELEASE} - exit ;; - VAX*:ULTRIX*:*:*) - echo vax-dec-ultrix${UNAME_RELEASE} - exit ;; - 2020:CLIX:*:* | 2430:CLIX:*:*) - echo clipper-intergraph-clix${UNAME_RELEASE} - exit ;; - mips:*:*:UMIPS | mips:*:*:RISCos) - eval $set_cc_for_build - sed 's/^ //' << EOF >$dummy.c -#ifdef __cplusplus -#include /* for printf() prototype */ - int main (int argc, char *argv[]) { -#else - int main (argc, argv) int argc; char *argv[]; { -#endif - #if defined (host_mips) && defined (MIPSEB) - #if defined (SYSTYPE_SYSV) - printf ("mips-mips-riscos%ssysv\n", argv[1]); exit (0); - #endif - #if defined (SYSTYPE_SVR4) - printf ("mips-mips-riscos%ssvr4\n", argv[1]); exit (0); - #endif - #if defined (SYSTYPE_BSD43) || defined(SYSTYPE_BSD) - printf ("mips-mips-riscos%sbsd\n", argv[1]); exit (0); - #endif - #endif - exit (-1); - } -EOF - $CC_FOR_BUILD -o $dummy $dummy.c && - dummyarg=`echo "${UNAME_RELEASE}" | sed -n 's/\([0-9]*\).*/\1/p'` && - SYSTEM_NAME=`$dummy $dummyarg` && - { echo "$SYSTEM_NAME"; exit; } - echo mips-mips-riscos${UNAME_RELEASE} - exit ;; - Motorola:PowerMAX_OS:*:*) - echo powerpc-motorola-powermax - exit ;; - Motorola:*:4.3:PL8-*) - echo powerpc-harris-powermax - exit ;; - Night_Hawk:*:*:PowerMAX_OS | Synergy:PowerMAX_OS:*:*) - echo powerpc-harris-powermax - exit ;; - Night_Hawk:Power_UNIX:*:*) - echo powerpc-harris-powerunix - exit ;; - m88k:CX/UX:7*:*) - echo m88k-harris-cxux7 - exit ;; - m88k:*:4*:R4*) - echo m88k-motorola-sysv4 - exit ;; - m88k:*:3*:R3*) - echo m88k-motorola-sysv3 - exit ;; - AViiON:dgux:*:*) - # DG/UX returns AViiON for all architectures - UNAME_PROCESSOR=`/usr/bin/uname -p` - if [ $UNAME_PROCESSOR = mc88100 ] || [ $UNAME_PROCESSOR = mc88110 ] - then - if [ ${TARGET_BINARY_INTERFACE}x = m88kdguxelfx ] || \ - [ ${TARGET_BINARY_INTERFACE}x = x ] - then - echo m88k-dg-dgux${UNAME_RELEASE} - else - echo m88k-dg-dguxbcs${UNAME_RELEASE} - fi - else - echo i586-dg-dgux${UNAME_RELEASE} - fi - exit ;; - M88*:DolphinOS:*:*) # DolphinOS (SVR3) - echo m88k-dolphin-sysv3 - exit ;; - M88*:*:R3*:*) - # Delta 88k system running SVR3 - echo m88k-motorola-sysv3 - exit ;; - XD88*:*:*:*) # Tektronix XD88 system running UTekV (SVR3) - echo m88k-tektronix-sysv3 - exit ;; - Tek43[0-9][0-9]:UTek:*:*) # Tektronix 4300 system running UTek (BSD) - echo m68k-tektronix-bsd - exit ;; - *:IRIX*:*:*) - echo mips-sgi-irix`echo ${UNAME_RELEASE}|sed -e 's/-/_/g'` - exit ;; - ????????:AIX?:[12].1:2) # AIX 2.2.1 or AIX 2.1.1 is RT/PC AIX. - echo romp-ibm-aix # uname -m gives an 8 hex-code CPU id - exit ;; # Note that: echo "'`uname -s`'" gives 'AIX ' - i*86:AIX:*:*) - echo i386-ibm-aix - exit ;; - ia64:AIX:*:*) - if [ -x /usr/bin/oslevel ] ; then - IBM_REV=`/usr/bin/oslevel` - else - IBM_REV=${UNAME_VERSION}.${UNAME_RELEASE} - fi - echo ${UNAME_MACHINE}-ibm-aix${IBM_REV} - exit ;; - *:AIX:2:3) - if grep bos325 /usr/include/stdio.h >/dev/null 2>&1; then - eval $set_cc_for_build - sed 's/^ //' << EOF >$dummy.c - #include - - main() - { - if (!__power_pc()) - exit(1); - puts("powerpc-ibm-aix3.2.5"); - exit(0); - } -EOF - if $CC_FOR_BUILD -o $dummy $dummy.c && SYSTEM_NAME=`$dummy` - then - echo "$SYSTEM_NAME" - else - echo rs6000-ibm-aix3.2.5 - fi - elif grep bos324 /usr/include/stdio.h >/dev/null 2>&1; then - echo rs6000-ibm-aix3.2.4 - else - echo rs6000-ibm-aix3.2 - fi - exit ;; - *:AIX:*:[4567]) - IBM_CPU_ID=`/usr/sbin/lsdev -C -c processor -S available | sed 1q | awk '{ print $1 }'` - if /usr/sbin/lsattr -El ${IBM_CPU_ID} | grep ' POWER' >/dev/null 2>&1; then - IBM_ARCH=rs6000 - else - IBM_ARCH=powerpc - fi - if [ -x /usr/bin/oslevel ] ; then - IBM_REV=`/usr/bin/oslevel` - else - IBM_REV=${UNAME_VERSION}.${UNAME_RELEASE} - fi - echo ${IBM_ARCH}-ibm-aix${IBM_REV} - exit ;; - *:AIX:*:*) - echo rs6000-ibm-aix - exit ;; - ibmrt:4.4BSD:*|romp-ibm:BSD:*) - echo romp-ibm-bsd4.4 - exit ;; - ibmrt:*BSD:*|romp-ibm:BSD:*) # covers RT/PC BSD and - echo romp-ibm-bsd${UNAME_RELEASE} # 4.3 with uname added to - exit ;; # report: romp-ibm BSD 4.3 - *:BOSX:*:*) - echo rs6000-bull-bosx - exit ;; - DPX/2?00:B.O.S.:*:*) - echo m68k-bull-sysv3 - exit ;; - 9000/[34]??:4.3bsd:1.*:*) - echo m68k-hp-bsd - exit ;; - hp300:4.4BSD:*:* | 9000/[34]??:4.3bsd:2.*:*) - echo m68k-hp-bsd4.4 - exit ;; - 9000/[34678]??:HP-UX:*:*) - HPUX_REV=`echo ${UNAME_RELEASE}|sed -e 's/[^.]*.[0B]*//'` - case "${UNAME_MACHINE}" in - 9000/31? ) HP_ARCH=m68000 ;; - 9000/[34]?? ) HP_ARCH=m68k ;; - 9000/[678][0-9][0-9]) - if [ -x /usr/bin/getconf ]; then - sc_cpu_version=`/usr/bin/getconf SC_CPU_VERSION 2>/dev/null` - sc_kernel_bits=`/usr/bin/getconf SC_KERNEL_BITS 2>/dev/null` - case "${sc_cpu_version}" in - 523) HP_ARCH="hppa1.0" ;; # CPU_PA_RISC1_0 - 528) HP_ARCH="hppa1.1" ;; # CPU_PA_RISC1_1 - 532) # CPU_PA_RISC2_0 - case "${sc_kernel_bits}" in - 32) HP_ARCH="hppa2.0n" ;; - 64) HP_ARCH="hppa2.0w" ;; - '') HP_ARCH="hppa2.0" ;; # HP-UX 10.20 - esac ;; - esac - fi - if [ "${HP_ARCH}" = "" ]; then - eval $set_cc_for_build - sed 's/^ //' << EOF >$dummy.c - - #define _HPUX_SOURCE - #include - #include - - int main () - { - #if defined(_SC_KERNEL_BITS) - long bits = sysconf(_SC_KERNEL_BITS); - #endif - long cpu = sysconf (_SC_CPU_VERSION); - - switch (cpu) - { - case CPU_PA_RISC1_0: puts ("hppa1.0"); break; - case CPU_PA_RISC1_1: puts ("hppa1.1"); break; - case CPU_PA_RISC2_0: - #if defined(_SC_KERNEL_BITS) - switch (bits) - { - case 64: puts ("hppa2.0w"); break; - case 32: puts ("hppa2.0n"); break; - default: puts ("hppa2.0"); break; - } break; - #else /* !defined(_SC_KERNEL_BITS) */ - puts ("hppa2.0"); break; - #endif - default: puts ("hppa1.0"); break; - } - exit (0); - } -EOF - (CCOPTS= $CC_FOR_BUILD -o $dummy $dummy.c 2>/dev/null) && HP_ARCH=`$dummy` - test -z "$HP_ARCH" && HP_ARCH=hppa - fi ;; - esac - if [ ${HP_ARCH} = "hppa2.0w" ] - then - eval $set_cc_for_build - - # hppa2.0w-hp-hpux* has a 64-bit kernel and a compiler generating - # 32-bit code. hppa64-hp-hpux* has the same kernel and a compiler - # generating 64-bit code. GNU and HP use different nomenclature: - # - # $ CC_FOR_BUILD=cc ./config.guess - # => hppa2.0w-hp-hpux11.23 - # $ CC_FOR_BUILD="cc +DA2.0w" ./config.guess - # => hppa64-hp-hpux11.23 - - if echo __LP64__ | (CCOPTS= $CC_FOR_BUILD -E - 2>/dev/null) | - grep -q __LP64__ - then - HP_ARCH="hppa2.0w" - else - HP_ARCH="hppa64" - fi - fi - echo ${HP_ARCH}-hp-hpux${HPUX_REV} - exit ;; - ia64:HP-UX:*:*) - HPUX_REV=`echo ${UNAME_RELEASE}|sed -e 's/[^.]*.[0B]*//'` - echo ia64-hp-hpux${HPUX_REV} - exit ;; - 3050*:HI-UX:*:*) - eval $set_cc_for_build - sed 's/^ //' << EOF >$dummy.c - #include - int - main () - { - long cpu = sysconf (_SC_CPU_VERSION); - /* The order matters, because CPU_IS_HP_MC68K erroneously returns - true for CPU_PA_RISC1_0. CPU_IS_PA_RISC returns correct - results, however. */ - if (CPU_IS_PA_RISC (cpu)) - { - switch (cpu) - { - case CPU_PA_RISC1_0: puts ("hppa1.0-hitachi-hiuxwe2"); break; - case CPU_PA_RISC1_1: puts ("hppa1.1-hitachi-hiuxwe2"); break; - case CPU_PA_RISC2_0: puts ("hppa2.0-hitachi-hiuxwe2"); break; - default: puts ("hppa-hitachi-hiuxwe2"); break; - } - } - else if (CPU_IS_HP_MC68K (cpu)) - puts ("m68k-hitachi-hiuxwe2"); - else puts ("unknown-hitachi-hiuxwe2"); - exit (0); - } -EOF - $CC_FOR_BUILD -o $dummy $dummy.c && SYSTEM_NAME=`$dummy` && - { echo "$SYSTEM_NAME"; exit; } - echo unknown-hitachi-hiuxwe2 - exit ;; - 9000/7??:4.3bsd:*:* | 9000/8?[79]:4.3bsd:*:* ) - echo hppa1.1-hp-bsd - exit ;; - 9000/8??:4.3bsd:*:*) - echo hppa1.0-hp-bsd - exit ;; - *9??*:MPE/iX:*:* | *3000*:MPE/iX:*:*) - echo hppa1.0-hp-mpeix - exit ;; - hp7??:OSF1:*:* | hp8?[79]:OSF1:*:* ) - echo hppa1.1-hp-osf - exit ;; - hp8??:OSF1:*:*) - echo hppa1.0-hp-osf - exit ;; - i*86:OSF1:*:*) - if [ -x /usr/sbin/sysversion ] ; then - echo ${UNAME_MACHINE}-unknown-osf1mk - else - echo ${UNAME_MACHINE}-unknown-osf1 - fi - exit ;; - parisc*:Lites*:*:*) - echo hppa1.1-hp-lites - exit ;; - C1*:ConvexOS:*:* | convex:ConvexOS:C1*:*) - echo c1-convex-bsd - exit ;; - C2*:ConvexOS:*:* | convex:ConvexOS:C2*:*) - if getsysinfo -f scalar_acc - then echo c32-convex-bsd - else echo c2-convex-bsd - fi - exit ;; - C34*:ConvexOS:*:* | convex:ConvexOS:C34*:*) - echo c34-convex-bsd - exit ;; - C38*:ConvexOS:*:* | convex:ConvexOS:C38*:*) - echo c38-convex-bsd - exit ;; - C4*:ConvexOS:*:* | convex:ConvexOS:C4*:*) - echo c4-convex-bsd - exit ;; - CRAY*Y-MP:*:*:*) - echo ymp-cray-unicos${UNAME_RELEASE} | sed -e 's/\.[^.]*$/.X/' - exit ;; - CRAY*[A-Z]90:*:*:*) - echo ${UNAME_MACHINE}-cray-unicos${UNAME_RELEASE} \ - | sed -e 's/CRAY.*\([A-Z]90\)/\1/' \ - -e y/ABCDEFGHIJKLMNOPQRSTUVWXYZ/abcdefghijklmnopqrstuvwxyz/ \ - -e 's/\.[^.]*$/.X/' - exit ;; - CRAY*TS:*:*:*) - echo t90-cray-unicos${UNAME_RELEASE} | sed -e 's/\.[^.]*$/.X/' - exit ;; - CRAY*T3E:*:*:*) - echo alphaev5-cray-unicosmk${UNAME_RELEASE} | sed -e 's/\.[^.]*$/.X/' - exit ;; - CRAY*SV1:*:*:*) - echo sv1-cray-unicos${UNAME_RELEASE} | sed -e 's/\.[^.]*$/.X/' - exit ;; - *:UNICOS/mp:*:*) - echo craynv-cray-unicosmp${UNAME_RELEASE} | sed -e 's/\.[^.]*$/.X/' - exit ;; - F30[01]:UNIX_System_V:*:* | F700:UNIX_System_V:*:*) - FUJITSU_PROC=`uname -m | tr 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz'` - FUJITSU_SYS=`uname -p | tr 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz' | sed -e 's/\///'` - FUJITSU_REL=`echo ${UNAME_RELEASE} | sed -e 's/ /_/'` - echo "${FUJITSU_PROC}-fujitsu-${FUJITSU_SYS}${FUJITSU_REL}" - exit ;; - 5000:UNIX_System_V:4.*:*) - FUJITSU_SYS=`uname -p | tr 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz' | sed -e 's/\///'` - FUJITSU_REL=`echo ${UNAME_RELEASE} | tr 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz' | sed -e 's/ /_/'` - echo "sparc-fujitsu-${FUJITSU_SYS}${FUJITSU_REL}" - exit ;; - i*86:BSD/386:*:* | i*86:BSD/OS:*:* | *:Ascend\ Embedded/OS:*:*) - echo ${UNAME_MACHINE}-pc-bsdi${UNAME_RELEASE} - exit ;; - sparc*:BSD/OS:*:*) - echo sparc-unknown-bsdi${UNAME_RELEASE} - exit ;; - *:BSD/OS:*:*) - echo ${UNAME_MACHINE}-unknown-bsdi${UNAME_RELEASE} - exit ;; - *:FreeBSD:*:*) - UNAME_PROCESSOR=`/usr/bin/uname -p` - case ${UNAME_PROCESSOR} in - amd64) - echo x86_64-unknown-freebsd`echo ${UNAME_RELEASE}|sed -e 's/[-(].*//'` ;; - *) - echo ${UNAME_PROCESSOR}-unknown-freebsd`echo ${UNAME_RELEASE}|sed -e 's/[-(].*//'` ;; - esac - exit ;; - i*:CYGWIN*:*) - echo ${UNAME_MACHINE}-pc-cygwin - exit ;; - *:MINGW64*:*) - echo ${UNAME_MACHINE}-pc-mingw64 - exit ;; - *:MINGW*:*) - echo ${UNAME_MACHINE}-pc-mingw32 - exit ;; - i*:MSYS*:*) - echo ${UNAME_MACHINE}-pc-msys - exit ;; - i*:windows32*:*) - # uname -m includes "-pc" on this system. - echo ${UNAME_MACHINE}-mingw32 - exit ;; - i*:PW*:*) - echo ${UNAME_MACHINE}-pc-pw32 - exit ;; - *:Interix*:*) - case ${UNAME_MACHINE} in - x86) - echo i586-pc-interix${UNAME_RELEASE} - exit ;; - authenticamd | genuineintel | EM64T) - echo x86_64-unknown-interix${UNAME_RELEASE} - exit ;; - IA64) - echo ia64-unknown-interix${UNAME_RELEASE} - exit ;; - esac ;; - [345]86:Windows_95:* | [345]86:Windows_98:* | [345]86:Windows_NT:*) - echo i${UNAME_MACHINE}-pc-mks - exit ;; - 8664:Windows_NT:*) - echo x86_64-pc-mks - exit ;; - i*:Windows_NT*:* | Pentium*:Windows_NT*:*) - # How do we know it's Interix rather than the generic POSIX subsystem? - # It also conflicts with pre-2.0 versions of AT&T UWIN. Should we - # UNAME_MACHINE based on the output of uname instead of i386? - echo i586-pc-interix - exit ;; - i*:UWIN*:*) - echo ${UNAME_MACHINE}-pc-uwin - exit ;; - amd64:CYGWIN*:*:* | x86_64:CYGWIN*:*:*) - echo x86_64-unknown-cygwin - exit ;; - p*:CYGWIN*:*) - echo powerpcle-unknown-cygwin - exit ;; - prep*:SunOS:5.*:*) - echo powerpcle-unknown-solaris2`echo ${UNAME_RELEASE}|sed -e 's/[^.]*//'` - exit ;; - *:GNU:*:*) - # the GNU system - echo `echo ${UNAME_MACHINE}|sed -e 's,[-/].*$,,'`-unknown-${LIBC}`echo ${UNAME_RELEASE}|sed -e 's,/.*$,,'` - exit ;; - *:GNU/*:*:*) - # other systems with GNU libc and userland - echo ${UNAME_MACHINE}-unknown-`echo ${UNAME_SYSTEM} | sed 's,^[^/]*/,,' | tr '[A-Z]' '[a-z]'``echo ${UNAME_RELEASE}|sed -e 's/[-(].*//'`-${LIBC} - exit ;; - i*86:Minix:*:*) - echo ${UNAME_MACHINE}-pc-minix - exit ;; - aarch64:Linux:*:*) - echo ${UNAME_MACHINE}-unknown-linux-${LIBC} - exit ;; - aarch64_be:Linux:*:*) - UNAME_MACHINE=aarch64_be - echo ${UNAME_MACHINE}-unknown-linux-${LIBC} - exit ;; - alpha:Linux:*:*) - case `sed -n '/^cpu model/s/^.*: \(.*\)/\1/p' < /proc/cpuinfo` in - EV5) UNAME_MACHINE=alphaev5 ;; - EV56) UNAME_MACHINE=alphaev56 ;; - PCA56) UNAME_MACHINE=alphapca56 ;; - PCA57) UNAME_MACHINE=alphapca56 ;; - EV6) UNAME_MACHINE=alphaev6 ;; - EV67) UNAME_MACHINE=alphaev67 ;; - EV68*) UNAME_MACHINE=alphaev68 ;; - esac - objdump --private-headers /bin/sh | grep -q ld.so.1 - if test "$?" = 0 ; then LIBC="gnulibc1" ; fi - echo ${UNAME_MACHINE}-unknown-linux-${LIBC} - exit ;; - arc:Linux:*:* | arceb:Linux:*:*) - echo ${UNAME_MACHINE}-unknown-linux-${LIBC} - exit ;; - arm*:Linux:*:*) - eval $set_cc_for_build - if echo __ARM_EABI__ | $CC_FOR_BUILD -E - 2>/dev/null \ - | grep -q __ARM_EABI__ - then - echo ${UNAME_MACHINE}-unknown-linux-${LIBC} - else - if echo __ARM_PCS_VFP | $CC_FOR_BUILD -E - 2>/dev/null \ - | grep -q __ARM_PCS_VFP - then - echo ${UNAME_MACHINE}-unknown-linux-${LIBC}eabi - else - echo ${UNAME_MACHINE}-unknown-linux-${LIBC}eabihf - fi - fi - exit ;; - avr32*:Linux:*:*) - echo ${UNAME_MACHINE}-unknown-linux-${LIBC} - exit ;; - cris:Linux:*:*) - echo ${UNAME_MACHINE}-axis-linux-${LIBC} - exit ;; - crisv32:Linux:*:*) - echo ${UNAME_MACHINE}-axis-linux-${LIBC} - exit ;; - frv:Linux:*:*) - echo ${UNAME_MACHINE}-unknown-linux-${LIBC} - exit ;; - hexagon:Linux:*:*) - echo ${UNAME_MACHINE}-unknown-linux-${LIBC} - exit ;; - i*86:Linux:*:*) - echo ${UNAME_MACHINE}-pc-linux-${LIBC} - exit ;; - ia64:Linux:*:*) - echo ${UNAME_MACHINE}-unknown-linux-${LIBC} - exit ;; - m32r*:Linux:*:*) - echo ${UNAME_MACHINE}-unknown-linux-${LIBC} - exit ;; - m68*:Linux:*:*) - echo ${UNAME_MACHINE}-unknown-linux-${LIBC} - exit ;; - mips:Linux:*:* | mips64:Linux:*:*) - eval $set_cc_for_build - sed 's/^ //' << EOF >$dummy.c - #undef CPU - #undef ${UNAME_MACHINE} - #undef ${UNAME_MACHINE}el - #if defined(__MIPSEL__) || defined(__MIPSEL) || defined(_MIPSEL) || defined(MIPSEL) - CPU=${UNAME_MACHINE}el - #else - #if defined(__MIPSEB__) || defined(__MIPSEB) || defined(_MIPSEB) || defined(MIPSEB) - CPU=${UNAME_MACHINE} - #else - CPU= - #endif - #endif -EOF - eval `$CC_FOR_BUILD -E $dummy.c 2>/dev/null | grep '^CPU'` - test x"${CPU}" != x && { echo "${CPU}-unknown-linux-${LIBC}"; exit; } - ;; - or1k:Linux:*:*) - echo ${UNAME_MACHINE}-unknown-linux-${LIBC} - exit ;; - or32:Linux:*:*) - echo ${UNAME_MACHINE}-unknown-linux-${LIBC} - exit ;; - padre:Linux:*:*) - echo sparc-unknown-linux-${LIBC} - exit ;; - parisc64:Linux:*:* | hppa64:Linux:*:*) - echo hppa64-unknown-linux-${LIBC} - exit ;; - parisc:Linux:*:* | hppa:Linux:*:*) - # Look for CPU level - case `grep '^cpu[^a-z]*:' /proc/cpuinfo 2>/dev/null | cut -d' ' -f2` in - PA7*) echo hppa1.1-unknown-linux-${LIBC} ;; - PA8*) echo hppa2.0-unknown-linux-${LIBC} ;; - *) echo hppa-unknown-linux-${LIBC} ;; - esac - exit ;; - ppc64:Linux:*:*) - echo powerpc64-unknown-linux-${LIBC} - exit ;; - ppc:Linux:*:*) - echo powerpc-unknown-linux-${LIBC} - exit ;; - ppc64le:Linux:*:*) - echo powerpc64le-unknown-linux-${LIBC} - exit ;; - ppcle:Linux:*:*) - echo powerpcle-unknown-linux-${LIBC} - exit ;; - s390:Linux:*:* | s390x:Linux:*:*) - echo ${UNAME_MACHINE}-ibm-linux-${LIBC} - exit ;; - sh64*:Linux:*:*) - echo ${UNAME_MACHINE}-unknown-linux-${LIBC} - exit ;; - sh*:Linux:*:*) - echo ${UNAME_MACHINE}-unknown-linux-${LIBC} - exit ;; - sparc:Linux:*:* | sparc64:Linux:*:*) - echo ${UNAME_MACHINE}-unknown-linux-${LIBC} - exit ;; - tile*:Linux:*:*) - echo ${UNAME_MACHINE}-unknown-linux-${LIBC} - exit ;; - vax:Linux:*:*) - echo ${UNAME_MACHINE}-dec-linux-${LIBC} - exit ;; - x86_64:Linux:*:*) - echo ${UNAME_MACHINE}-unknown-linux-${LIBC} - exit ;; - xtensa*:Linux:*:*) - echo ${UNAME_MACHINE}-unknown-linux-${LIBC} - exit ;; - i*86:DYNIX/ptx:4*:*) - # ptx 4.0 does uname -s correctly, with DYNIX/ptx in there. - # earlier versions are messed up and put the nodename in both - # sysname and nodename. - echo i386-sequent-sysv4 - exit ;; - i*86:UNIX_SV:4.2MP:2.*) - # Unixware is an offshoot of SVR4, but it has its own version - # number series starting with 2... - # I am not positive that other SVR4 systems won't match this, - # I just have to hope. -- rms. - # Use sysv4.2uw... so that sysv4* matches it. - echo ${UNAME_MACHINE}-pc-sysv4.2uw${UNAME_VERSION} - exit ;; - i*86:OS/2:*:*) - # If we were able to find `uname', then EMX Unix compatibility - # is probably installed. - echo ${UNAME_MACHINE}-pc-os2-emx - exit ;; - i*86:XTS-300:*:STOP) - echo ${UNAME_MACHINE}-unknown-stop - exit ;; - i*86:atheos:*:*) - echo ${UNAME_MACHINE}-unknown-atheos - exit ;; - i*86:syllable:*:*) - echo ${UNAME_MACHINE}-pc-syllable - exit ;; - i*86:LynxOS:2.*:* | i*86:LynxOS:3.[01]*:* | i*86:LynxOS:4.[02]*:*) - echo i386-unknown-lynxos${UNAME_RELEASE} - exit ;; - i*86:*DOS:*:*) - echo ${UNAME_MACHINE}-pc-msdosdjgpp - exit ;; - i*86:*:4.*:* | i*86:SYSTEM_V:4.*:*) - UNAME_REL=`echo ${UNAME_RELEASE} | sed 's/\/MP$//'` - if grep Novell /usr/include/link.h >/dev/null 2>/dev/null; then - echo ${UNAME_MACHINE}-univel-sysv${UNAME_REL} - else - echo ${UNAME_MACHINE}-pc-sysv${UNAME_REL} - fi - exit ;; - i*86:*:5:[678]*) - # UnixWare 7.x, OpenUNIX and OpenServer 6. - case `/bin/uname -X | grep "^Machine"` in - *486*) UNAME_MACHINE=i486 ;; - *Pentium) UNAME_MACHINE=i586 ;; - *Pent*|*Celeron) UNAME_MACHINE=i686 ;; - esac - echo ${UNAME_MACHINE}-unknown-sysv${UNAME_RELEASE}${UNAME_SYSTEM}${UNAME_VERSION} - exit ;; - i*86:*:3.2:*) - if test -f /usr/options/cb.name; then - UNAME_REL=`sed -n 's/.*Version //p' /dev/null >/dev/null ; then - UNAME_REL=`(/bin/uname -X|grep Release|sed -e 's/.*= //')` - (/bin/uname -X|grep i80486 >/dev/null) && UNAME_MACHINE=i486 - (/bin/uname -X|grep '^Machine.*Pentium' >/dev/null) \ - && UNAME_MACHINE=i586 - (/bin/uname -X|grep '^Machine.*Pent *II' >/dev/null) \ - && UNAME_MACHINE=i686 - (/bin/uname -X|grep '^Machine.*Pentium Pro' >/dev/null) \ - && UNAME_MACHINE=i686 - echo ${UNAME_MACHINE}-pc-sco$UNAME_REL - else - echo ${UNAME_MACHINE}-pc-sysv32 - fi - exit ;; - pc:*:*:*) - # Left here for compatibility: - # uname -m prints for DJGPP always 'pc', but it prints nothing about - # the processor, so we play safe by assuming i586. - # Note: whatever this is, it MUST be the same as what config.sub - # prints for the "djgpp" host, or else GDB configury will decide that - # this is a cross-build. - echo i586-pc-msdosdjgpp - exit ;; - Intel:Mach:3*:*) - echo i386-pc-mach3 - exit ;; - paragon:*:*:*) - echo i860-intel-osf1 - exit ;; - i860:*:4.*:*) # i860-SVR4 - if grep Stardent /usr/include/sys/uadmin.h >/dev/null 2>&1 ; then - echo i860-stardent-sysv${UNAME_RELEASE} # Stardent Vistra i860-SVR4 - else # Add other i860-SVR4 vendors below as they are discovered. - echo i860-unknown-sysv${UNAME_RELEASE} # Unknown i860-SVR4 - fi - exit ;; - mini*:CTIX:SYS*5:*) - # "miniframe" - echo m68010-convergent-sysv - exit ;; - mc68k:UNIX:SYSTEM5:3.51m) - echo m68k-convergent-sysv - exit ;; - M680?0:D-NIX:5.3:*) - echo m68k-diab-dnix - exit ;; - M68*:*:R3V[5678]*:*) - test -r /sysV68 && { echo 'm68k-motorola-sysv'; exit; } ;; - 3[345]??:*:4.0:3.0 | 3[34]??A:*:4.0:3.0 | 3[34]??,*:*:4.0:3.0 | 3[34]??/*:*:4.0:3.0 | 4400:*:4.0:3.0 | 4850:*:4.0:3.0 | SKA40:*:4.0:3.0 | SDS2:*:4.0:3.0 | SHG2:*:4.0:3.0 | S7501*:*:4.0:3.0) - OS_REL='' - test -r /etc/.relid \ - && OS_REL=.`sed -n 's/[^ ]* [^ ]* \([0-9][0-9]\).*/\1/p' < /etc/.relid` - /bin/uname -p 2>/dev/null | grep 86 >/dev/null \ - && { echo i486-ncr-sysv4.3${OS_REL}; exit; } - /bin/uname -p 2>/dev/null | /bin/grep entium >/dev/null \ - && { echo i586-ncr-sysv4.3${OS_REL}; exit; } ;; - 3[34]??:*:4.0:* | 3[34]??,*:*:4.0:*) - /bin/uname -p 2>/dev/null | grep 86 >/dev/null \ - && { echo i486-ncr-sysv4; exit; } ;; - NCR*:*:4.2:* | MPRAS*:*:4.2:*) - OS_REL='.3' - test -r /etc/.relid \ - && OS_REL=.`sed -n 's/[^ ]* [^ ]* \([0-9][0-9]\).*/\1/p' < /etc/.relid` - /bin/uname -p 2>/dev/null | grep 86 >/dev/null \ - && { echo i486-ncr-sysv4.3${OS_REL}; exit; } - /bin/uname -p 2>/dev/null | /bin/grep entium >/dev/null \ - && { echo i586-ncr-sysv4.3${OS_REL}; exit; } - /bin/uname -p 2>/dev/null | /bin/grep pteron >/dev/null \ - && { echo i586-ncr-sysv4.3${OS_REL}; exit; } ;; - m68*:LynxOS:2.*:* | m68*:LynxOS:3.0*:*) - echo m68k-unknown-lynxos${UNAME_RELEASE} - exit ;; - mc68030:UNIX_System_V:4.*:*) - echo m68k-atari-sysv4 - exit ;; - TSUNAMI:LynxOS:2.*:*) - echo sparc-unknown-lynxos${UNAME_RELEASE} - exit ;; - rs6000:LynxOS:2.*:*) - echo rs6000-unknown-lynxos${UNAME_RELEASE} - exit ;; - PowerPC:LynxOS:2.*:* | PowerPC:LynxOS:3.[01]*:* | PowerPC:LynxOS:4.[02]*:*) - echo powerpc-unknown-lynxos${UNAME_RELEASE} - exit ;; - SM[BE]S:UNIX_SV:*:*) - echo mips-dde-sysv${UNAME_RELEASE} - exit ;; - RM*:ReliantUNIX-*:*:*) - echo mips-sni-sysv4 - exit ;; - RM*:SINIX-*:*:*) - echo mips-sni-sysv4 - exit ;; - *:SINIX-*:*:*) - if uname -p 2>/dev/null >/dev/null ; then - UNAME_MACHINE=`(uname -p) 2>/dev/null` - echo ${UNAME_MACHINE}-sni-sysv4 - else - echo ns32k-sni-sysv - fi - exit ;; - PENTIUM:*:4.0*:*) # Unisys `ClearPath HMP IX 4000' SVR4/MP effort - # says - echo i586-unisys-sysv4 - exit ;; - *:UNIX_System_V:4*:FTX*) - # From Gerald Hewes . - # How about differentiating between stratus architectures? -djm - echo hppa1.1-stratus-sysv4 - exit ;; - *:*:*:FTX*) - # From seanf@swdc.stratus.com. - echo i860-stratus-sysv4 - exit ;; - i*86:VOS:*:*) - # From Paul.Green@stratus.com. - echo ${UNAME_MACHINE}-stratus-vos - exit ;; - *:VOS:*:*) - # From Paul.Green@stratus.com. - echo hppa1.1-stratus-vos - exit ;; - mc68*:A/UX:*:*) - echo m68k-apple-aux${UNAME_RELEASE} - exit ;; - news*:NEWS-OS:6*:*) - echo mips-sony-newsos6 - exit ;; - R[34]000:*System_V*:*:* | R4000:UNIX_SYSV:*:* | R*000:UNIX_SV:*:*) - if [ -d /usr/nec ]; then - echo mips-nec-sysv${UNAME_RELEASE} - else - echo mips-unknown-sysv${UNAME_RELEASE} - fi - exit ;; - BeBox:BeOS:*:*) # BeOS running on hardware made by Be, PPC only. - echo powerpc-be-beos - exit ;; - BeMac:BeOS:*:*) # BeOS running on Mac or Mac clone, PPC only. - echo powerpc-apple-beos - exit ;; - BePC:BeOS:*:*) # BeOS running on Intel PC compatible. - echo i586-pc-beos - exit ;; - BePC:Haiku:*:*) # Haiku running on Intel PC compatible. - echo i586-pc-haiku - exit ;; - x86_64:Haiku:*:*) - echo x86_64-unknown-haiku - exit ;; - SX-4:SUPER-UX:*:*) - echo sx4-nec-superux${UNAME_RELEASE} - exit ;; - SX-5:SUPER-UX:*:*) - echo sx5-nec-superux${UNAME_RELEASE} - exit ;; - SX-6:SUPER-UX:*:*) - echo sx6-nec-superux${UNAME_RELEASE} - exit ;; - SX-7:SUPER-UX:*:*) - echo sx7-nec-superux${UNAME_RELEASE} - exit ;; - SX-8:SUPER-UX:*:*) - echo sx8-nec-superux${UNAME_RELEASE} - exit ;; - SX-8R:SUPER-UX:*:*) - echo sx8r-nec-superux${UNAME_RELEASE} - exit ;; - Power*:Rhapsody:*:*) - echo powerpc-apple-rhapsody${UNAME_RELEASE} - exit ;; - *:Rhapsody:*:*) - echo ${UNAME_MACHINE}-apple-rhapsody${UNAME_RELEASE} - exit ;; - *:Darwin:*:*) - UNAME_PROCESSOR=`uname -p` || UNAME_PROCESSOR=unknown - eval $set_cc_for_build - if test "$UNAME_PROCESSOR" = unknown ; then - UNAME_PROCESSOR=powerpc - fi - if [ "$CC_FOR_BUILD" != 'no_compiler_found' ]; then - if (echo '#ifdef __LP64__'; echo IS_64BIT_ARCH; echo '#endif') | \ - (CCOPTS= $CC_FOR_BUILD -E - 2>/dev/null) | \ - grep IS_64BIT_ARCH >/dev/null - then - case $UNAME_PROCESSOR in - i386) UNAME_PROCESSOR=x86_64 ;; - powerpc) UNAME_PROCESSOR=powerpc64 ;; - esac - fi - fi - echo ${UNAME_PROCESSOR}-apple-darwin${UNAME_RELEASE} - exit ;; - *:procnto*:*:* | *:QNX:[0123456789]*:*) - UNAME_PROCESSOR=`uname -p` - if test "$UNAME_PROCESSOR" = "x86"; then - UNAME_PROCESSOR=i386 - UNAME_MACHINE=pc - fi - echo ${UNAME_PROCESSOR}-${UNAME_MACHINE}-nto-qnx${UNAME_RELEASE} - exit ;; - *:QNX:*:4*) - echo i386-pc-qnx - exit ;; - NEO-?:NONSTOP_KERNEL:*:*) - echo neo-tandem-nsk${UNAME_RELEASE} - exit ;; - NSE-*:NONSTOP_KERNEL:*:*) - echo nse-tandem-nsk${UNAME_RELEASE} - exit ;; - NSR-?:NONSTOP_KERNEL:*:*) - echo nsr-tandem-nsk${UNAME_RELEASE} - exit ;; - *:NonStop-UX:*:*) - echo mips-compaq-nonstopux - exit ;; - BS2000:POSIX*:*:*) - echo bs2000-siemens-sysv - exit ;; - DS/*:UNIX_System_V:*:*) - echo ${UNAME_MACHINE}-${UNAME_SYSTEM}-${UNAME_RELEASE} - exit ;; - *:Plan9:*:*) - # "uname -m" is not consistent, so use $cputype instead. 386 - # is converted to i386 for consistency with other x86 - # operating systems. - if test "$cputype" = "386"; then - UNAME_MACHINE=i386 - else - UNAME_MACHINE="$cputype" - fi - echo ${UNAME_MACHINE}-unknown-plan9 - exit ;; - *:TOPS-10:*:*) - echo pdp10-unknown-tops10 - exit ;; - *:TENEX:*:*) - echo pdp10-unknown-tenex - exit ;; - KS10:TOPS-20:*:* | KL10:TOPS-20:*:* | TYPE4:TOPS-20:*:*) - echo pdp10-dec-tops20 - exit ;; - XKL-1:TOPS-20:*:* | TYPE5:TOPS-20:*:*) - echo pdp10-xkl-tops20 - exit ;; - *:TOPS-20:*:*) - echo pdp10-unknown-tops20 - exit ;; - *:ITS:*:*) - echo pdp10-unknown-its - exit ;; - SEI:*:*:SEIUX) - echo mips-sei-seiux${UNAME_RELEASE} - exit ;; - *:DragonFly:*:*) - echo ${UNAME_MACHINE}-unknown-dragonfly`echo ${UNAME_RELEASE}|sed -e 's/[-(].*//'` - exit ;; - *:*VMS:*:*) - UNAME_MACHINE=`(uname -p) 2>/dev/null` - case "${UNAME_MACHINE}" in - A*) echo alpha-dec-vms ; exit ;; - I*) echo ia64-dec-vms ; exit ;; - V*) echo vax-dec-vms ; exit ;; - esac ;; - *:XENIX:*:SysV) - echo i386-pc-xenix - exit ;; - i*86:skyos:*:*) - echo ${UNAME_MACHINE}-pc-skyos`echo ${UNAME_RELEASE}` | sed -e 's/ .*$//' - exit ;; - i*86:rdos:*:*) - echo ${UNAME_MACHINE}-pc-rdos - exit ;; - i*86:AROS:*:*) - echo ${UNAME_MACHINE}-pc-aros - exit ;; - x86_64:VMkernel:*:*) - echo ${UNAME_MACHINE}-unknown-esx - exit ;; -esac - -eval $set_cc_for_build -cat >$dummy.c < -# include -#endif -main () -{ -#if defined (sony) -#if defined (MIPSEB) - /* BFD wants "bsd" instead of "newsos". Perhaps BFD should be changed, - I don't know.... */ - printf ("mips-sony-bsd\n"); exit (0); -#else -#include - printf ("m68k-sony-newsos%s\n", -#ifdef NEWSOS4 - "4" -#else - "" -#endif - ); exit (0); -#endif -#endif - -#if defined (__arm) && defined (__acorn) && defined (__unix) - printf ("arm-acorn-riscix\n"); exit (0); -#endif - -#if defined (hp300) && !defined (hpux) - printf ("m68k-hp-bsd\n"); exit (0); -#endif - -#if defined (NeXT) -#if !defined (__ARCHITECTURE__) -#define __ARCHITECTURE__ "m68k" -#endif - int version; - version=`(hostinfo | sed -n 's/.*NeXT Mach \([0-9]*\).*/\1/p') 2>/dev/null`; - if (version < 4) - printf ("%s-next-nextstep%d\n", __ARCHITECTURE__, version); - else - printf ("%s-next-openstep%d\n", __ARCHITECTURE__, version); - exit (0); -#endif - -#if defined (MULTIMAX) || defined (n16) -#if defined (UMAXV) - printf ("ns32k-encore-sysv\n"); exit (0); -#else -#if defined (CMU) - printf ("ns32k-encore-mach\n"); exit (0); -#else - printf ("ns32k-encore-bsd\n"); exit (0); -#endif -#endif -#endif - -#if defined (__386BSD__) - printf ("i386-pc-bsd\n"); exit (0); -#endif - -#if defined (sequent) -#if defined (i386) - printf ("i386-sequent-dynix\n"); exit (0); -#endif -#if defined (ns32000) - printf ("ns32k-sequent-dynix\n"); exit (0); -#endif -#endif - -#if defined (_SEQUENT_) - struct utsname un; - - uname(&un); - - if (strncmp(un.version, "V2", 2) == 0) { - printf ("i386-sequent-ptx2\n"); exit (0); - } - if (strncmp(un.version, "V1", 2) == 0) { /* XXX is V1 correct? */ - printf ("i386-sequent-ptx1\n"); exit (0); - } - printf ("i386-sequent-ptx\n"); exit (0); - -#endif - -#if defined (vax) -# if !defined (ultrix) -# include -# if defined (BSD) -# if BSD == 43 - printf ("vax-dec-bsd4.3\n"); exit (0); -# else -# if BSD == 199006 - printf ("vax-dec-bsd4.3reno\n"); exit (0); -# else - printf ("vax-dec-bsd\n"); exit (0); -# endif -# endif -# else - printf ("vax-dec-bsd\n"); exit (0); -# endif -# else - printf ("vax-dec-ultrix\n"); exit (0); -# endif -#endif - -#if defined (alliant) && defined (i860) - printf ("i860-alliant-bsd\n"); exit (0); -#endif - - exit (1); -} -EOF - -$CC_FOR_BUILD -o $dummy $dummy.c 2>/dev/null && SYSTEM_NAME=`$dummy` && - { echo "$SYSTEM_NAME"; exit; } - -# Apollos put the system type in the environment. - -test -d /usr/apollo && { echo ${ISP}-apollo-${SYSTYPE}; exit; } - -# Convex versions that predate uname can use getsysinfo(1) - -if [ -x /usr/convex/getsysinfo ] -then - case `getsysinfo -f cpu_type` in - c1*) - echo c1-convex-bsd - exit ;; - c2*) - if getsysinfo -f scalar_acc - then echo c32-convex-bsd - else echo c2-convex-bsd - fi - exit ;; - c34*) - echo c34-convex-bsd - exit ;; - c38*) - echo c38-convex-bsd - exit ;; - c4*) - echo c4-convex-bsd - exit ;; - esac -fi - -cat >&2 < in order to provide the needed -information to handle your system. - -config.guess timestamp = $timestamp - -uname -m = `(uname -m) 2>/dev/null || echo unknown` -uname -r = `(uname -r) 2>/dev/null || echo unknown` -uname -s = `(uname -s) 2>/dev/null || echo unknown` -uname -v = `(uname -v) 2>/dev/null || echo unknown` - -/usr/bin/uname -p = `(/usr/bin/uname -p) 2>/dev/null` -/bin/uname -X = `(/bin/uname -X) 2>/dev/null` - -hostinfo = `(hostinfo) 2>/dev/null` -/bin/universe = `(/bin/universe) 2>/dev/null` -/usr/bin/arch -k = `(/usr/bin/arch -k) 2>/dev/null` -/bin/arch = `(/bin/arch) 2>/dev/null` -/usr/bin/oslevel = `(/usr/bin/oslevel) 2>/dev/null` -/usr/convex/getsysinfo = `(/usr/convex/getsysinfo) 2>/dev/null` - -UNAME_MACHINE = ${UNAME_MACHINE} -UNAME_RELEASE = ${UNAME_RELEASE} -UNAME_SYSTEM = ${UNAME_SYSTEM} -UNAME_VERSION = ${UNAME_VERSION} -EOF - -exit 1 - -# Local variables: -# eval: (add-hook 'write-file-hooks 'time-stamp) -# time-stamp-start: "timestamp='" -# time-stamp-format: "%:y-%02m-%02d" -# time-stamp-end: "'" -# End: diff --git a/build/config.mk b/build/config.mk deleted file mode 100644 index 0a720600..00000000 --- a/build/config.mk +++ /dev/null @@ -1,100 +0,0 @@ -# we need this line or else default 'make' behavior will only generate host-config.mk -do-make-all: all - -$(TOP)/build/host-config.mk: - @(host_triple=`$(TOP)/build/config.guess`; \ - echo HOST_TRIPLE:=$$host_triple > $@; \ - echo $$host_triple | awk '{split($$0,a,"-"); print "HOST_CPU:=" a[1] "\nHOST_VENDOR:=" a[2] "\nHOST_OS:=" a[3] "\n"}' >> $@) - --include $(TOP)/build/host-config.mk - -LLVM_SUFFIX?=-16.0.6 - -# we don't care about the version here -HOST_OS:=$(patsubst darwin%,darwin,$(HOST_OS)) - -PRODUCT_NAME=EchoJS -PRODUCT_VERSION=0.1.0 - -PRODUCT_RELEASE_NOTES_URL=http://toshokelectric.com/echojs/release_notes -PRODUCT_GITHUB_URL=https://github.com/toshok/echo-js -PRODUCT_EMAIL=toshok@toshokelectric.com -ORGANIZATION=com.toshokelectric - - -PRODUCT_name:=$(shell echo $(PRODUCT_NAME) | tr [:upper:] [:lower:]) - -PRODUCT_UTI=$(ORGANIZATION).$(PRODUCT_NAME) - -# the place where we stuff everything -PRODUCT_INSTALL_ROOT=/Library/Frameworks/$(PRODUCT_NAME).framework - -MKDIR=mkdir -p -INSTALL=install -CP=cp -CC?=clang -CXX?=clang++ - -CFLAGS=-g -O0 -Wall -I. -Wno-unused-function -Wno-unused-variable - -MIN_IOS_VERSION=8.0 -MIN_OSX_VERSION=10.10 - -DEVELOPER_ROOT?=/Applications/Xcode.app/Contents/Developer -IOS_SDK_VERSION?=8.3 - -ifeq ($(HOST_OS),linux) -EJS_RUNLOOP_IMPL?=libuv -else -EJS_RUNLOOP_IMPL=darwin -endif - -ifeq ($(HOST_CPU),x86_64) -LINUX_ARCH=-arch x86_64 -LINUX_CFLAGS=$(CFLAGS) -DTARGET_CPU_AMD64=1 -DEJS_BITS_PER_WORD=64 -DIS_LITTLE_ENDIAN=1 -D_GNU_SOURCE -else -LINUX_ARCH=-arch x86 -LINUX_CFLAGS=$(CFLAGS) -DTARGET_CPU_X86=1 -DEJS_BITS_PER_WORD=32 -DIS_LITTLE_ENDIAN=1 -D_GNU_SOURCE -endif - -OSX_ARCH=-arch aarch64 -OSX_MTRIPLE="arm64-apple-macosx(MIN_OSX_VERSION).0" -OSX_CFLAGS=$(CFLAGS) -DOSX=1 -DTARGET_CPU_AARCH64=1 -DEJS_BITS_PER_WORD=64 -DIS_LITTLE_ENDIAN=1 -D_XOPEN_SOURCE -Wno-deprecated-declarations - -IOSSIM_ARCH=-arch x86_64 -IOSSIM_TRIPLE=x86_64-apple-darwin -IOSSIM_MTRIPLE="x86_64-apple-ios$(MIN_IOS_VERSION).0" -IOSSIM_ARCH_FLAGS=-DTARGET_CPU_X86=1 -DEJS_BITS_PER_WORD=32 -DIS_LITTLE_ENDIAN=1 -IOSSIM_ROOT=$(DEVELOPER_ROOT)/Platforms/iPhoneSimulator.platform/Developer -IOSSIM_BIN=$(IOSSIM_ROOT)/usr/bin -IOSSIM_SYSROOT=$(IOSSIM_ROOT)/SDKs/iPhoneSimulator$(IOS_SDK_VERSION).sdk - -IOSDEV_ARCH=-arch armv7 -IOSDEV_TRIPLE=armv7-apple-darwin -IOSDEV_MTRIPLE="thumbv7-apple-ios$(MIN_IOS_VERSION).0" -IOSDEV_ARCH_FLAGS=-mthumb -DTARGET_CPU_ARM=1 -DEJS_BITS_PER_WORD=32 -DIS_LITTLE_ENDIAN=1 -IOSDEV_ROOT=$(DEVELOPER_ROOT)/Platforms/iPhoneOS.platform/Developer -IOSDEV_BIN=$(IOSDEV_ROOT)/usr/bin -IOSDEV_SYSROOT=$(IOSDEV_ROOT)/SDKs/iPhoneOS$(IOS_SDK_VERSION).sdk - -IOSDEVS_ARCH=-arch armv7s -IOSDEVS_TRIPLE=armv7s-apple-darwin -IOSDEVS_MTRIPLE="thumbv7s-apple-ios$(MIN_IOS_VERSION).0" -IOSDEVS_ARCH_FLAGS=-mthumb -DTARGET_CPU_ARM=1 -DEJS_BITS_PER_WORD=64 -DIS_LITTLE_ENDIAN=1 -IOSDEVS_ROOT=$(DEVELOPER_ROOT)/Platforms/iPhoneOS.platform/Developer -IOSDEVS_BIN=$(IOSDEV_ROOT)/usr/bin -IOSDEVS_SYSROOT=$(IOSDEV_ROOT)/SDKs/iPhoneOS$(IOS_SDK_VERSION).sdk - -IOSSIM_CFLAGS=$(IOSSIM_ARCH) $(IOSSIM_ARCH_FLAGS) $(CFLAGS) -DIOS=1 -isysroot $(IOSSIM_SYSROOT) -miphoneos-version-min=$(MIN_IOS_VERSION) -D_XOPEN_SOURCE -Wno-deprecated-declarations -IOSDEV_CFLAGS=$(IOSDEV_ARCH) $(IOSDEV_ARCH_FLAGS) $(CFLAGS) -DIOS=1 -isysroot $(IOSDEV_SYSROOT) -miphoneos-version-min=$(MIN_IOS_VERSION) -D_XOPEN_SOURCE -Wno-deprecated-declarations -IOSDEVS_CFLAGS=$(IOSDEVS_ARCH) $(IOSDEVS_ARCH_FLAGS) $(CFLAGS) -DIOS=1 -isysroot $(IOSDEVS_SYSROOT) -miphoneos-version-min=$(MIN_IOS_VERSION) -D_XOPEN_SOURCE -Wno-deprecated-declarations - -# directories used during make install -prefix?=/usr/local - -bindir:=$(DESTDIR)$(prefix)/bin -includedir:=$(DESTDIR)$(prefix)/include -libdir:=$(DESTDIR)$(prefix)/lib -archlibdir:=$(libdir)/$(HOST_CPU)-$(HOST_OS) - --include $(TOP)/build/config-local.mk diff --git a/build/iOS.cmake b/build/iOS.cmake deleted file mode 100644 index 5fe64190..00000000 --- a/build/iOS.cmake +++ /dev/null @@ -1,213 +0,0 @@ -# This file is based off of the Platform/Darwin.cmake and Platform/UnixPaths.cmake -# files which are included with CMake 2.8.4 -# It has been altered for iOS development - -# Options: -# -# IOS_PLATFORM = OS (default) or SIMULATOR or SIMULATOR64 -# This decides if SDKS will be selected from the iPhoneOS.platform or iPhoneSimulator.platform folders -# OS - the default, used to build for iPhone and iPad physical devices, which have an arm arch. -# SIMULATOR - used to build for the Simulator platforms, which have an x86 arch. -# -# CMAKE_IOS_DEVELOPER_ROOT = automatic(default) or /path/to/platform/Developer folder -# By default this location is automatcially chosen based on the IOS_PLATFORM value above. -# If set manually, it will override the default location and force the user of a particular Developer Platform -# -# CMAKE_IOS_SDK_ROOT = automatic(default) or /path/to/platform/Developer/SDKs/SDK folder -# By default this location is automatcially chosen based on the CMAKE_IOS_DEVELOPER_ROOT value. -# In this case it will always be the most up-to-date SDK found in the CMAKE_IOS_DEVELOPER_ROOT path. -# If set manually, this will force the use of a specific SDK version - -# Macros: -# -# set_xcode_property (TARGET XCODE_PROPERTY XCODE_VALUE) -# A convenience macro for setting xcode specific properties on targets -# example: set_xcode_property (myioslib IPHONEOS_DEPLOYMENT_TARGET "3.1") -# -# find_host_package (PROGRAM ARGS) -# A macro used to find executable programs on the host system, not within the iOS environment. -# Thanks to the android-cmake project for providing the command - -# Standard settings -set (CMAKE_SYSTEM_NAME Darwin) -set (CMAKE_SYSTEM_VERSION 1) -set (UNIX True) -set (APPLE True) -set (IOS True) - -# Required as of cmake 2.8.10 -set (CMAKE_OSX_DEPLOYMENT_TARGET "" CACHE STRING "Force unset of the deployment target for iOS" FORCE) - -# Determine the cmake host system version so we know where to find the iOS SDKs -find_program (CMAKE_UNAME uname /bin /usr/bin /usr/local/bin) -if (CMAKE_UNAME) - exec_program(uname ARGS -r OUTPUT_VARIABLE CMAKE_HOST_SYSTEM_VERSION) - string (REGEX REPLACE "^([0-9]+)\\.([0-9]+).*$" "\\1" DARWIN_MAJOR_VERSION "${CMAKE_HOST_SYSTEM_VERSION}") -endif (CMAKE_UNAME) - -# Force the compilers to gcc for iOS -include (CMakeForceCompiler) -CMAKE_FORCE_C_COMPILER (/usr/bin/clang Apple) -CMAKE_FORCE_CXX_COMPILER (/usr/bin/clang++ Apple) -set(CMAKE_AR ar CACHE FILEPATH "" FORCE) - -# Skip the platform compiler checks for cross compiling -set (CMAKE_CXX_COMPILER_WORKS TRUE) -set (CMAKE_C_COMPILER_WORKS TRUE) - -# All iOS/Darwin specific settings - some may be redundant -set (CMAKE_SHARED_LIBRARY_PREFIX "lib") -set (CMAKE_SHARED_LIBRARY_SUFFIX ".dylib") -set (CMAKE_SHARED_MODULE_PREFIX "lib") -set (CMAKE_SHARED_MODULE_SUFFIX ".so") -set (CMAKE_MODULE_EXISTS 1) -set (CMAKE_DL_LIBS "") - -set (CMAKE_C_OSX_COMPATIBILITY_VERSION_FLAG "-compatibility_version ") -set (CMAKE_C_OSX_CURRENT_VERSION_FLAG "-current_version ") -set (CMAKE_CXX_OSX_COMPATIBILITY_VERSION_FLAG "${CMAKE_C_OSX_COMPATIBILITY_VERSION_FLAG}") -set (CMAKE_CXX_OSX_CURRENT_VERSION_FLAG "${CMAKE_C_OSX_CURRENT_VERSION_FLAG}") - -# Hidden visibilty is required for cxx on iOS -set (CMAKE_C_FLAGS_INIT "") -set (CMAKE_CXX_FLAGS_INIT "-fvisibility=hidden -fvisibility-inlines-hidden") - -set (CMAKE_C_LINK_FLAGS "-Wl,-search_paths_first ${CMAKE_C_LINK_FLAGS}") -set (CMAKE_CXX_LINK_FLAGS "-Wl,-search_paths_first ${CMAKE_CXX_LINK_FLAGS}") - -set (CMAKE_PLATFORM_HAS_INSTALLNAME 1) -set (CMAKE_SHARED_LIBRARY_CREATE_C_FLAGS "-dynamiclib -headerpad_max_install_names") -set (CMAKE_SHARED_MODULE_CREATE_C_FLAGS "-bundle -headerpad_max_install_names") -set (CMAKE_SHARED_MODULE_LOADER_C_FLAG "-Wl,-bundle_loader,") -set (CMAKE_SHARED_MODULE_LOADER_CXX_FLAG "-Wl,-bundle_loader,") -set (CMAKE_FIND_LIBRARY_SUFFIXES ".dylib" ".so" ".a") - -# hack: if a new cmake (which uses CMAKE_INSTALL_NAME_TOOL) runs on an old build tree -# (where install_name_tool was hardcoded) and where CMAKE_INSTALL_NAME_TOOL isn't in the cache -# and still cmake didn't fail in CMakeFindBinUtils.cmake (because it isn't rerun) -# hardcode CMAKE_INSTALL_NAME_TOOL here to install_name_tool, so it behaves as it did before, Alex -if (NOT DEFINED CMAKE_INSTALL_NAME_TOOL) - find_program(CMAKE_INSTALL_NAME_TOOL install_name_tool) -endif (NOT DEFINED CMAKE_INSTALL_NAME_TOOL) - -# Setup iOS platform unless specified manually with IOS_PLATFORM -if (NOT DEFINED IOS_PLATFORM) - set (IOS_PLATFORM "OS") -endif (NOT DEFINED IOS_PLATFORM) -set (IOS_PLATFORM ${IOS_PLATFORM} CACHE STRING "Type of iOS Platform") - -# Setup building for arm64 or not -if (NOT DEFINED BUILD_ARM64) - set (BUILD_ARM64 true) -endif (NOT DEFINED BUILD_ARM64) -set (BUILD_ARM64 ${BUILD_ARM64} CACHE STRING "Build arm64 arch or not") - -# Check the platform selection and setup for developer root -if (${IOS_PLATFORM} STREQUAL "OS") - set (IOS_PLATFORM_LOCATION "iPhoneOS.platform") - - # This causes the installers to properly locate the output libraries - set (CMAKE_XCODE_EFFECTIVE_PLATFORMS "-iphoneos") -elseif (${IOS_PLATFORM} STREQUAL "SIMULATOR") - set (SIMULATOR true) - set (IOS_PLATFORM_LOCATION "iPhoneSimulator.platform") - - # This causes the installers to properly locate the output libraries - set (CMAKE_XCODE_EFFECTIVE_PLATFORMS "-iphonesimulator") -elseif (${IOS_PLATFORM} STREQUAL "SIMULATOR64") - set (SIMULATOR true) - set (IOS_PLATFORM_LOCATION "iPhoneSimulator.platform") - - # This causes the installers to properly locate the output libraries - set (CMAKE_XCODE_EFFECTIVE_PLATFORMS "-iphonesimulator") -else (${IOS_PLATFORM} STREQUAL "OS") - message (FATAL_ERROR "Unsupported IOS_PLATFORM value selected. Please choose OS or SIMULATOR") -endif (${IOS_PLATFORM} STREQUAL "OS") - -# Setup iOS developer location unless specified manually with CMAKE_IOS_DEVELOPER_ROOT -# Note Xcode 4.3 changed the installation location, choose the most recent one available -exec_program(/usr/bin/xcode-select ARGS -print-path OUTPUT_VARIABLE CMAKE_XCODE_DEVELOPER_DIR) -set (XCODE_POST_43_ROOT "${CMAKE_XCODE_DEVELOPER_DIR}/Platforms/${IOS_PLATFORM_LOCATION}/Developer") -set (XCODE_PRE_43_ROOT "/Developer/Platforms/${IOS_PLATFORM_LOCATION}/Developer") -if (NOT DEFINED CMAKE_IOS_DEVELOPER_ROOT) - if (EXISTS ${XCODE_POST_43_ROOT}) - set (CMAKE_IOS_DEVELOPER_ROOT ${XCODE_POST_43_ROOT}) - elseif(EXISTS ${XCODE_PRE_43_ROOT}) - set (CMAKE_IOS_DEVELOPER_ROOT ${XCODE_PRE_43_ROOT}) - endif (EXISTS ${XCODE_POST_43_ROOT}) -endif (NOT DEFINED CMAKE_IOS_DEVELOPER_ROOT) -set (CMAKE_IOS_DEVELOPER_ROOT ${CMAKE_IOS_DEVELOPER_ROOT} CACHE PATH "Location of iOS Platform") - -# Find and use the most recent iOS sdk unless specified manually with CMAKE_IOS_SDK_ROOT -if (NOT DEFINED CMAKE_IOS_SDK_ROOT) - file (GLOB _CMAKE_IOS_SDKS "${CMAKE_IOS_DEVELOPER_ROOT}/SDKs/*") - if (_CMAKE_IOS_SDKS) - list (SORT _CMAKE_IOS_SDKS) - list (REVERSE _CMAKE_IOS_SDKS) - list (GET _CMAKE_IOS_SDKS 0 CMAKE_IOS_SDK_ROOT) - else (_CMAKE_IOS_SDKS) - message (FATAL_ERROR "No iOS SDK's found in default search path ${CMAKE_IOS_DEVELOPER_ROOT}. Manually set CMAKE_IOS_SDK_ROOT or install the iOS SDK.") - endif (_CMAKE_IOS_SDKS) - message (STATUS "Toolchain using default iOS SDK: ${CMAKE_IOS_SDK_ROOT}") -endif (NOT DEFINED CMAKE_IOS_SDK_ROOT) -set (CMAKE_IOS_SDK_ROOT ${CMAKE_IOS_SDK_ROOT} CACHE PATH "Location of the selected iOS SDK") - -# Set the sysroot default to the most recent SDK -set (CMAKE_OSX_SYSROOT ${CMAKE_IOS_SDK_ROOT} CACHE PATH "Sysroot used for iOS support") - -# set the architecture for iOS -if (${IOS_PLATFORM} STREQUAL "OS") - set (IOS_ARCH armv7) # XXX(toshok) armv7s arm64 -elseif (${IOS_PLATFORM} STREQUAL "SIMULATOR") - set (IOS_ARCH i386) -elseif (${IOS_PLATFORM} STREQUAL "SIMULATOR64") - set (IOS_ARCH x86_64) -endif (${IOS_PLATFORM} STREQUAL "OS") - -#set (CMAKE_OSX_ARCHITECTURES ${IOS_ARCH} CACHE string "Build architecture for iOS") - -set (CMAKE_CXX_FLAGS "-arch ${IOS_ARCH} -isysroot ${IOS_SYSROOT} -miphoneos-version-min=${MIN_IOS_VERSION}") -set (CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS} CACHE STRING "ios c++ flags") -set (CMAKE_C_FLAGS "-arch ${IOS_ARCH} -isysroot ${IOS_SYSROOT} -miphoneos-version-min=${MIN_IOS_VERSION}") -set (CMAKE_C_FLAGS ${CMAKE_C_FLAGS} CACHE STRING "ios c flags") - -# Set the find root to the iOS developer roots and to user defined paths -set (CMAKE_FIND_ROOT_PATH ${CMAKE_IOS_DEVELOPER_ROOT} ${CMAKE_IOS_SDK_ROOT} ${CMAKE_PREFIX_PATH} CACHE string "iOS find search path root") - -# default to searching for frameworks first -set (CMAKE_FIND_FRAMEWORK FIRST) - -# set up the default search directories for frameworks -set (CMAKE_SYSTEM_FRAMEWORK_PATH - ${CMAKE_IOS_SDK_ROOT}/System/Library/Frameworks - ${CMAKE_IOS_SDK_ROOT}/System/Library/PrivateFrameworks - ${CMAKE_IOS_SDK_ROOT}/Developer/Library/Frameworks -) - -# only search the iOS sdks, not the remainder of the host filesystem -set (CMAKE_FIND_ROOT_PATH_MODE_PROGRAM ONLY) -set (CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) -set (CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) - - -# This little macro lets you set any XCode specific property -macro (set_xcode_property TARGET XCODE_PROPERTY XCODE_VALUE) - set_property (TARGET ${TARGET} PROPERTY XCODE_ATTRIBUTE_${XCODE_PROPERTY} ${XCODE_VALUE}) -endmacro (set_xcode_property) - - -# This macro lets you find executable programs on the host system -macro (find_host_package) - set (CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) - set (CMAKE_FIND_ROOT_PATH_MODE_LIBRARY NEVER) - set (CMAKE_FIND_ROOT_PATH_MODE_INCLUDE NEVER) - set (IOS FALSE) - - find_package(${ARGN}) - - set (IOS TRUE) - set (CMAKE_FIND_ROOT_PATH_MODE_PROGRAM ONLY) - set (CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) - set (CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) -endmacro (find_host_package) - diff --git a/build/rules.mk b/build/rules.mk deleted file mode 100644 index b18dd988..00000000 --- a/build/rules.mk +++ /dev/null @@ -1,45 +0,0 @@ -all: all-local all-recurse all-hook -clean: clean-local clean-recurse clean-hook -install: install-local install-recurse install-hook -dist: dist-local dist-recurse dist-hook - -all-local:: -clean-local:: -install-local:: -dist-local:: - -all-recurse:: all-local -clean-recurse:: clean-local -install-recurse:: install-local -dist-recurse:: dist-local - -all-hook:: all-local all-recurse -clean-hook:: clean-local clean-recurse -install-hook:: install-local install-recurse -dist-hook:: dist-local dist-recurse - -RECURSE_INTO_SUBDIRS= \ - @target=`echo $@ | sed -e s/-recurse//`; \ - for i in $(SUBDIRS); do \ - echo Making $$target in $$i; \ - $(MAKE) -C $$i $$target || exit 1; \ - done - -ifneq ($(SUBDIRS),) -all-recurse:: - $(RECURSE_INTO_SUBDIRS) - -clean-recurse:: - $(RECURSE_INTO_SUBDIRS) - -install-recurse:: - $(RECURSE_INTO_SUBDIRS) - -dist-recurse:: - $(RECURSE_INTO_SUBDIRS) -endif - -.PHONY: all all-recurse all-hook -.PHONY: clean clean-recurse clean-hook -.PHONY: install install-recurse install-hook -.PHONY: dist dist-recurse dist-hook diff --git a/build/utils.mk b/build/utils.mk deleted file mode 100644 index 5b485da6..00000000 --- a/build/utils.mk +++ /dev/null @@ -1,21 +0,0 @@ -replace=-e "s,@$1@,$($1),g" - -dosed=sed $(call replace,ORGANIZATION) \ - $(call replace,PRODUCT_RELEASE_NOTES_URL) \ - $(call replace,PRODUCT_VERSION) \ - $(call replace,PRODUCT_INSTALL_ROOT) \ - $(call replace,PRODUCT_NAME) \ - $(call replace,PRODUCT_UTI) \ - $(call replace,PRODUCT_GITHUB_URL) \ - $(call replace,PRODUCT_EMAIL) \ - $(call replace,PRODUCT_name) \ - $(call replace,INSTALLKBYTES) \ - $(call replace,NUMFILES) - -# arg1 = input path -# arg2 = output path -define rewrite - @echo [GEN] $2 - @$(dosed) < $1 > $2 -endef - diff --git a/ci/install-node-osx.sh b/ci/install-node-osx.sh index 97dab367..f5ed9d80 100644 --- a/ci/install-node-osx.sh +++ b/ci/install-node-osx.sh @@ -1,4 +1,3 @@ set -e npm install -g node npm install -g node-gyp -npm install -g babel diff --git a/debian/changelog b/debian/changelog deleted file mode 100644 index d8ee9892..00000000 --- a/debian/changelog +++ /dev/null @@ -1,5 +0,0 @@ -echojs (0.0.1alpha11-1) trusty; urgency=low - - * Initial release - - -- Chris Toshok Thu, 22 Jan 2015 03:50:17 +0000 diff --git a/debian/compat b/debian/compat deleted file mode 100644 index ec635144..00000000 --- a/debian/compat +++ /dev/null @@ -1 +0,0 @@ -9 diff --git a/debian/control b/debian/control deleted file mode 100644 index 8b2334b1..00000000 --- a/debian/control +++ /dev/null @@ -1,16 +0,0 @@ -Source: echojs -Section: devel -Priority: optional -Maintainer: Chris Toshok -Build-Depends: debhelper (>= 8.0.0), clang-3.4, llvm-3.4-dev, nodejs-legacy, node-gyp, npm, libunwind8-dev, libuv-dev, time -Standards-Version: 3.9.4 -Homepage: https://github.com/toshok/echojs -#Vcs-Git: git://git.debian.org/collab-maint/echojs.git -#Vcs-Browser: http://git.debian.org/?p=collab-maint/echojs.git;a=summary - -Package: echojs -Architecture: amd64 -Depends: ${shlibs:Depends}, ${misc:Depends}, llvm-3.4, clang-3.4, libuv-dev -Description: ES6 to native compiler - Ahead of time Javascript compiler supporting large subset of ES6 spec. Compiles directly - to statically linked executables containing all runtime code. diff --git a/debian/copyright b/debian/copyright deleted file mode 100644 index 387142fe..00000000 --- a/debian/copyright +++ /dev/null @@ -1,53 +0,0 @@ -Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ -Upstream-Name: echojs -Source: https://github.com/toshok/echojs - -Files: * -Copyright: 2012-2015 Chris Toshok - 2014-2015 Carlos Alberto Cortez - -License: - The MIT License (MIT) - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. -# If you want to use GPL v2 or later for the /debian/* files use -# the following clauses, or change it to suit. Delete these two lines -Files: debian/* -Copyright: 2015 unknown -License: GPL-2+ - This package is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - . - This package is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - . - You should have received a copy of the GNU General Public License - along with this program. If not, see - . - On Debian systems, the complete text of the GNU General - Public License version 2 can be found in "/usr/share/common-licenses/GPL-2". - -# Please also look if there are files or directories which have a -# different copyright/license attached and list them here. -# Please avoid to pick license terms that are more restrictive than the -# packaged work, as it may make Debian's contributions unacceptable upstream. diff --git a/debian/docs b/debian/docs deleted file mode 100644 index d99978a9..00000000 --- a/debian/docs +++ /dev/null @@ -1,2 +0,0 @@ -LICENSE.txt -README.md diff --git a/debian/patches/debian-doesnt-like-usrlocal b/debian/patches/debian-doesnt-like-usrlocal deleted file mode 100644 index 14b17949..00000000 --- a/debian/patches/debian-doesnt-like-usrlocal +++ /dev/null @@ -1,12 +0,0 @@ -Description: debian doesn't like things in /usr/local ---- echo-js-0.0.0alpha2.orig/build/config.mk -+++ echo-js-0.0.0alpha2/build/config.mk -@@ -78,7 +78,7 @@ IOSDEV_CFLAGS=$(IOSDEV_ARCH) $(IOSDEV_AR - IOSDEVS_CFLAGS=$(IOSDEVS_ARCH) $(IOSDEVS_ARCH_FLAGS) $(CFLAGS) -DIOS=1 -isysroot $(IOSDEVS_SYSROOT) -miphoneos-version-min=$(MIN_IOS_VERSION) - - # directories used during make install --prefix?=/usr/local -+prefix?=/usr - - bindir:=$(DESTDIR)$(prefix)/bin - includedir:=$(DESTDIR)$(prefix)/include diff --git a/debian/patches/debian-hostconfig.mk b/debian/patches/debian-hostconfig.mk deleted file mode 100644 index 166c2c8b..00000000 --- a/debian/patches/debian-hostconfig.mk +++ /dev/null @@ -1,9 +0,0 @@ -Description: host-config.mk for debian build ---- /dev/null -+++ echo-js-0.0.0alpha2/build/host-config.mk -@@ -0,0 +1,5 @@ -+HOST_TRIPLE:=x86_64-unknown-linux-gnu -+HOST_CPU:=x86_64 -+HOST_VENDOR:=unknown -+HOST_OS:=linux -+ diff --git a/debian/patches/series b/debian/patches/series deleted file mode 100644 index d298a0e7..00000000 --- a/debian/patches/series +++ /dev/null @@ -1,2 +0,0 @@ -debian-doesnt-like-usrlocal -debian-hostconfig.mk diff --git a/debian/rules b/debian/rules deleted file mode 100755 index 79fd842d..00000000 --- a/debian/rules +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/make -f -# -*- makefile -*- - -# Uncomment this to turn on verbose mode. -#export DH_VERBOSE=1 - -%: - dh $@ diff --git a/debian/source/format b/debian/source/format deleted file mode 100644 index 163aaf8d..00000000 --- a/debian/source/format +++ /dev/null @@ -1 +0,0 @@ -3.0 (quilt) diff --git a/debian/source/include-binaries b/debian/source/include-binaries deleted file mode 100644 index b1391e5e..00000000 --- a/debian/source/include-binaries +++ /dev/null @@ -1,20 +0,0 @@ -test/osx-test/HelloOSX.app/Contents/Resources/en.lproj/MainMenu.nib -test/osx-test/HelloOSX.app/Contents/Resources/moonlight.icns -escodegen/escodegen.browser.js -esprima/assets/images/autocomplete.png -pcre/testdata/grepbinary -pcre/testdata/saved16 -pcre/testdata/saved16BE-1 -pcre/testdata/saved16BE-2 -pcre/testdata/saved16LE-1 -pcre/testdata/saved16LE-2 -pcre/testdata/saved32 -pcre/testdata/saved32BE-1 -pcre/testdata/saved32BE-2 -pcre/testdata/saved32LE-1 -pcre/testdata/saved32LE-2 -pcre/testdata/saved8 -samples/trackmix/TrackMix.app/Contents/Resources/moonlight.icns -samples/trackmixcode/TrackMixCode.app/Contents/Resources/en.lproj/MainMenu.nib -samples/trackmixcode/TrackMixCode.app/Contents/Resources/moonlight.icns - diff --git a/defs.bzl b/defs.bzl new file mode 100644 index 00000000..4f7a1bab --- /dev/null +++ b/defs.bzl @@ -0,0 +1,116 @@ +# Shared definitions for the EchoJS buck2 build. + +def llvm_prefix(): + return read_config("llvm", "prefix", "/opt/homebrew/opt/llvm@16") + +def llvm_suffix(): + return read_config("llvm", "suffix", "") + +def llvm_bindir(): + return llvm_prefix() + "/bin" + +def llvm_bin(tool): + return "{}/{}{}".format(llvm_bindir(), tool, llvm_suffix()) + +# Triple as lib/triple.js Triple.toString() renders the host triple +# (arch-vendor-os). Used for the runtime/out/ directory the +# compiler looks in when running with --srcdir. +EJS_TRIPLE = select({ + "config//os:linux": select({ + "config//cpu:arm64": "arm64-unknown-linux", + "config//cpu:x86_64": "x86_64-unknown-linux", + }), + "config//os:macos": select({ + "config//cpu:arm64": "arm64-apple-macos", + "config//cpu:x86_64": "x86_64-apple-macos", + }), +}) + +# Triple.toShortString() (arch-os), which is what the Makefiles call +# SHORT_TRIPLE and what node-compat.ejs keys its module_file map on. +EJS_SHORT_TRIPLE = select({ + "config//os:linux": select({ + "config//cpu:arm64": "arm64-linux", + "config//cpu:x86_64": "x86_64-linux", + }), + "config//os:macos": select({ + "config//cpu:arm64": "arm64-macos", + "config//cpu:x86_64": "x86_64-macos", + }), +}) + +# Short os name used for the external-deps build directory names +# (pcre-macos, double-conversion-linux, ...). +EJS_OS = select({ + "config//os:linux": "linux", + "config//os:macos": "macos", +}) + +# GNU-style triple passed to autoconf --build (old config.guess scripts +# in the pcre submodule don't recognize arm64 macs). +GNU_TRIPLE = select({ + "config//os:linux": select({ + "config//cpu:arm64": "aarch64-unknown-linux-gnu", + "config//cpu:x86_64": "x86_64-unknown-linux-gnu", + }), + "config//os:macos": select({ + "config//cpu:arm64": "aarch64-apple-darwin", + "config//cpu:x86_64": "x86_64-apple-darwin", + }), +}) + +# -mtriple for llc when compiling the .ll runtime sources. +LLC_MTRIPLE = select({ + "config//os:linux": select({ + "config//cpu:arm64": "aarch64-unknown-linux-gnu", + "config//cpu:x86_64": "x86_64-unknown-linux-gnu", + }), + "config//os:macos": select({ + "config//cpu:arm64": "arm64-apple-macosx11.0.0", + "config//cpu:x86_64": "x86_64-apple-macosx11.0.0", + }), +}) + +# The runloop implementation baked into lib/host-config.js. +EJS_RUNLOOP_IMPL = select({ + "config//os:linux": "libuv", + "config//os:macos": "darwin", +}) + +# Mirrors CFLAGS + per-target defines from mk/config.mk — except the +# optimization level: the runtime moved -O0 -> -O2 at gc-plan P0 (the +# plan's "single cheapest runtime speedup"; scanner assumptions +# re-verified there — MARK_REGISTERS spills callee-saved registers, the +# ABI pins live-across-call values to stack/callee-saved, and interior +# pointers canonicalize in both the page and (since P0) LOS lookups). +EJS_COMPILER_FLAGS = [ + "-g", + "-O2", + "-Wall", + "-Wno-unused-function", + "-Wno-unused-variable", +] + select({ + "config//os:linux": [ + "-DTARGET_LINUX=1", + "-D_GNU_SOURCE", + ], + "config//os:macos": [ + "-DOSX=1", + "-DTARGET_MACOS=1", + "-D_XOPEN_SOURCE", + "-Wno-deprecated-declarations", + ], +}) + select({ + "config//cpu:arm64": [ + # both spellings are in use (ejs-gc.c vs ejs-node-compat.c) + "-DTARGET_CPU_ARM64=1", + "-DTARGET_CPU_AARCH64=1", + "-DEJS_BITS_PER_WORD=64", + "-DIS_LITTLE_ENDIAN=1", + ], + "config//cpu:x86_64": [ + "-DTARGET_CPU_AMD64=1", + "-DEJS_BITS_PER_WORD=64", + "-DIS_LITTLE_ENDIAN=1", + ], +}) diff --git a/docs/compiler-p1-results.md b/docs/compiler-p1-results.md new file mode 100644 index 00000000..082f91a6 --- /dev/null +++ b/docs/compiler-p1-results.md @@ -0,0 +1,155 @@ +# compiler-P1 results — optimizer residue (plans P5.4) + +Phase record for compiler-plan.md's compiler-P1: the SSA cleanups, +the type lattice, module-slot load CSE, and direct-call +devirtualization. Landed 2026-07-26 on `eir`. + +## What landed + +New passes (`lib/eir/cleanup.ts`, `lib/eir/devirt.ts`), wired in +`optimize.ts` / `integrate.ts`: + +- **Type lattice** (`computeLattice`): trust-free flat lattice over + boxed values — const kinds, fixed-result generic ops (`sub`/`mul`/ + `div`/`mod`/bit ops/`neg`/`unary_plus` → number; compares/`logical_not`/ + `instanceof`/`in`/`typeof_is` → boolean; allocation ops → object; + `make_closure` → function; `typeof` → string; `add`'s operand rule), + block-param meets iterated to fixpoint. No oracle input, so it is + sound (and fires) on flag-off compiles. +- **Cleanup fixpoint** (`cleanupFunction`, runs LAST in + `optimizeFunction` — after the region passes, for foldUnboxOfBox's + reason: folding arithmetic earlier perturbs the exact IR shapes the + region matchers verify): + - primitive-const folding, evaluated in the hosting engine (host and + runtime implement the same ES semantics for primitive arithmetic). + Fail-closed exclusions, each argued from a known host/runtime + divergence: no folds that mint a string from non-strings (number + formatting is the runtime's), no string relational compares + (collation), no equality over `-0` (the runtime's strict_eq leads + with a NaN-box tag compare, so `-0 === 0` is false there — the + math2.js xfail — and a self-hosted compiler would fold it the + runtime's way, breaking stage byte-identity); + - `typeof` folds matching the RUNTIME's mapping (`null` → `"null"`, + the documented quirk); + - `typeof x === "T"` → `typeof_is` (the op existed in ops.ts but was + never minted; the emit case now calls `_ejs_op_typeof_is_`); + - branch folding: cond_br on known-truthiness `to_boolean` + (consts; lattice undefined/null falsy, object/function truthy), + never-number `has_tag` FALSE folds, never-shaped `has_shape` + FALSE folds; `to_boolean(logical_not x)` inverts the branch + instead of calling `_ejs_op_not` + `_ejs_truthy`; + - trivial block-param pruning (the SSA form of copy propagation); + - **lattice-typed f64 lowering** — the "feed the low tier beyond + the oracle" item: generic add/sub/mul/div both of whose operands + are proven numbers compute unboxed with NO guard (`(a*1)+(b*1)` + emits `f64_add` on any compile); `lt`/`gt` feed cond_br through + `f64_lt` when the whole same-block lt/to_boolean/cond_br chain + rewrites; `unary_plus` on a proven number is the identity. + `EJS_NO_EIR_CLEANUP` bisects the whole group. +- **Module-slot load CSE** (`cseModuleSlotLoads`, runs BEFORE the + region passes): block-local availability with store-to-load + forwarding, killed at CALL-effect instructions; plus a dominance + tier over STABLE %self slots — exactly one static store, sitting in + the toplevel entry block. Stability argument: the module init flag + is set BEFORE the body runs (compiler.ts emitModuleResolution), so + the toplevel executes at most once per process; a suspended init's + remaining stores can only run after a callee returns, so a stable + slot never changes during any activation. Export-accessor setters + count as stores, so an externally-writable export never qualifies. + In the toplevel, every load the store comes-before folds to the + stored VALUE; in other functions, dominated loads fold to their + dominators. `EJS_NO_SLOT_CSE` bisects. +- **Devirtualization** (`devirtualizeModule`, module pass in + integrate.ts, runs AFTER specialization + ctor-sink so it never + starves the strictly-better call_typed rewrite): SSA-visible `call` + of a `make_closure` goes direct with the closure's env; a call + through a single-store %self slot goes direct with an undefined env + when the callee's %env param is entirely unused (load-observes-store + proven via the specialize.ts toplevel-entry prefix rule or + same-function dominance). What invoke_closure does that a direct + call skips: IS_FUNCTION (statically true) and the class-constructor + TypeError — so any function whose closure could reach + `set_constructor_kind_*` declines, and an unenumerable marking + operand declines the whole module (fail closed). `EJS_NO_DEVIRT` + bisects. + +## Fallout fixed en route + +The phase's passes were the first to exercise several dormant paths; +four real pre-existing bugs fell out (the EIR-flush "27 latent bugs" +precedent, continued): + +- **`Map.prototype.delete` was an unimplemented stub** + (runtime/ejs-map.c `_ejs_map_delete`: spec steps in comments, + `return _ejs_false;` since 2015). cleanup.ts's CSE was the first + compiler code to call Map.delete, so the SELF-HOSTED compiler's + availability-kill silently kept stale entries and folded reloads + across calls (stage1: updateassign1's compound-assign getter count, + proxies, Symbol.hasInstance — 8 suite failures). Implemented via + the Set.delete pattern (key/value → NO_ITER_VALUE magic; set/get/ + size/iteration already skip empties). +- **ejs-llvm had no FP IRBuilder bindings beyond createFAdd**. + Flag-off compiles never emitted f64 ops before the lattice pass, so + a SELF-HOSTED compile that reached emit's f64 cases read a missing + native method (boxed null) and threw "object not a function" — + `createFSub`/`createFMul`/`createFDiv`/`createFCmpOLT` (+ atoms) + added. (Under node, node-llvm always had them — stage0 green while + stage1 crashed, which is what made this hunt confusing.) +- **Emitter double-const cache `-0` collision** (compiler.ts + `loadDoubleEjsValue`): cache key was `num_${n}` and `String(-0)` is + `"0"` — a folded `-0` const emitted before a `+0` in the same + function hijacked its cache slot (caught by the lowtier lane: + `1/0` printed `-Infinity`). And the first fix's guard + (`n === 0 && 1/n < 0`) was itself disabled under self-host by the + strict_eq `-0 === 0` tag-compare quirk — the final test is + `1/n === -Infinity`, quirk-proof under both hosts. cleanup.ts's + `isNegZeroConst` uses the same form for the same reason. +- Two pre-existing guard-merge unit tests pinned the post-merge slow + chain as fully generic; the slow `add` over (proven-number) mul + results now lowers to f64, and the tests pin the new shape. + +Soundness holes found by the gates and closed: + +- has_tag FALSE-folding was removed from foldBranches: a boxed-repr + slot_store's verifier proof IS a dominating has_tag=false fact, and + folding the branch deleted the fact out from under the surviving + store (the --types lane failed to compile every class file). +- Suspension awareness in CSE: a desugared generator body's + activation can see the toplevel's remaining stores run mid-flight + (create generator → drive it → store → resume), so functions + containing generator_* runtime calls decline both the stable-slot + dominance tier and the stable-survives-CALL exemption. + +## Gate results (2026-07-28) + +- `//:test-eir` unit tests green (17 new: cleanup folds, lattice + lowering, trivial params, slot CSE incl. stability attacks and the + generator-suspension decline, devirt incl. ctor-kind and env-use + declines, bisect flags); `//:test-eir-lowtier` green. +- Full stage matrix green: `test-stage0` through `test-stage3` + + `test-stage1-shapes-off` — the stage2/stage3 fixed point survives + the compiler being optimized by (and running) the new passes. + New suite test `map6.js` pins the Map.delete fix under every stage. +- `--types` diff lane: 474 files — 473 identical, **0 divergent**, + 1 N/A (the standing tester.js esprima gap), 0 compile failures. +- Toplevel shape-region merging (the shapes-P3 note this phase + unblocks): `const p = {x:1,y:2}; console.log(p.x + p.y + p.x)` + compiles to 2 has_shape guards with CSE vs 3 without + (`p.x + p.x` after a store pair: 3 vs 4) — reloads no longer break + receiver identity at toplevel. +- Self-compile telemetry (node-hosted `-d` over the whole compiler, + 38 modules): 1,483 call sites devirtualized (esprima 878, + escodegen 225 — closure dispatch off the parser's hot paths), + 779 slot loads CSE'd, 580 lattice-typed ops lowered to f64, + 598 branches folded, 85 consts folded, 57 `typeof` tests rewritten + to typeof_is, 45 trivial params pruned. +- Benchmarks: + - **flag-off loop kernel** (`s = s + i*2 - i` ×50M, no --types): + **0.09s vs 1.3s with cleanup off (~14×), 2.8× faster than node** + (0.25s) — the lattice proves the loop-carried param is a number + (init const + f64-add back-edge meet) and the whole loop computes + unboxed with no guard, on a plain compile. + - types-bench2 (--types): 0.21s vs 0.26s with the new passes off + (~20%). + - Self-hosted self-compile: 57.9s — inside the gc-P4 56–64s band; + the new passes' compile-time cost is absorbed. diff --git a/docs/compiler-p2-results.md b/docs/compiler-p2-results.md new file mode 100644 index 00000000..1d542eb6 --- /dev/null +++ b/docs/compiler-p2-results.md @@ -0,0 +1,110 @@ +# compiler-P2 results — the TypeScript port, finished (P7.4) + +Phase: compiler-P2 (plans.md P7.4). Branch `eir`, 2026-07-29. + +The compiler sources were already strict TS (the EIR work's incremental +port); what remained was the tooling that still ran JS through babel, +and the residual JS entry points. Both are gone: **babel is no longer a +dependency of anything in the repo.** + +## What changed + +### 1. `//lib:generated`: the babel step is now tsc + +`lib/buck-gen-js.sh` used to run every file of the stage0 tree through +`@babel/cli` (preset-env, `modules: commonjs`), one process per file. +It now stages the `//lib:tsjs` ES-module tree (plus host-config and the +esprima/escodegen/estraverse/esutils externals), applies the +`"@llvm"`→`"llvm"` / `"@node-compat/"`→`""` rewrites with sed on the +way in, and runs **one** tsc invocation over the whole tree: +`--allowJs --module commonjs --esModuleInterop --target es2016` — no +type-checking (no `checkJs`), just the module conversion babel used to +do. `--esModuleInterop` matches babel's default/namespace-import +interop against CJS modules (the node-llvm addon, glob, ...). +TS 7 note: `--moduleResolution node10` is gone; the default for +`--module commonjs` resolves the extensionless relative imports fine. + +### 2. `// generator: babel-node` → `// generator: esm` + +Import-syntax tests can't run under plain node (extensionless relative +specifiers); babel-node's require hook used to transpile them during +expected-output generation. The tester now does it with tsc: transpile +the test plus its relative-import closure to CommonJS in a scratch dir +(`generateExpectedEsm` in tester.ts), copy the harness shim and driver +alongside (unconverted — the serializer runs byte-exact), and run +`node harness-run.js ` as before. The directive is +renamed in all 114 test files; `esm` names the test's need, not a tool. + +Closure resolution mirrors the compiler's: file first, then +`directory/index.js` (modules6). Two tests import from outside test/ +(`esprima-roundtrip{1,2}`, `../external-deps/...`) — both are +`skip-if: true` and were unrunnable under babel-node too; the esm +generator doesn't reach outside test/ (noted in esprima1.js). + +**Parity:** all 112 runnable esm tests generate byte-identical output +under babel-node and under the tsc path (the other 2 are the skipped +esprima-roundtrip pair). End-to-end through the real tester, a deleted +baseline regenerates byte-identical to the committed one. + +### 3. `test/tester.js` → `test/tester.ts` + +Ported under the repo's strict flag family (strict, +noUncheckedIndexedAccess, noImplicitOverride) with a small +`tester-deps.d.ts` (temp has no types; colors' chained `red.bold` isn't +in its shipped types). `test/tsconfig.json` holds the compile settings +(CommonJS output; skipLibCheck because glob's path-scurry .d.ts trips +over @types/node 26). buck-test-stage.sh compiles the staged copy in +place (`tsc -p "$WORK/test"`) before running it; the emitted +test/tester.js is gitignored. CI typechecks it (`tsc -p test +--noEmit`) next to the root config. + +Faithful port, plus: the dead `-s` range check (`< 0 && > 2`) now +actually validates 0..3; the unused CircleCI `running_in_ci` and the +collected-but-unused stderr buffer are gone; stage-index and +tests-array accesses are guarded (noUncheckedIndexedAccess). The +scheduler, xfail/skip-if/generator directive handling, wrapper +generation, and per-test TMPDIR behavior are unchanged. + +### 4. `runtime/gen-atoms.js` → `runtime/gen-atoms.ts` + +Compiled by the new `//runtime:gen-atoms-js` genrule (tsc, same strict +family); `//runtime:atoms` and `//ejs-llvm:atoms` consume the compiled +JS. Output verified byte-identical on both atoms headers. Included in +the root tsconfig typecheck. + +### 5. babel removed + +`@babel/cli`, `@babel/node`, `@babel/preset-env` dropped from +package.json (lock refreshed; `grep -c babel package-lock.json` = 0), +`.babelrc` deleted, ci.yml's babel-node PATH export removed, stale +"babel'd tree" comments updated across BUCK files and scripts. + +## What stays JS deliberately + +- `test/harness-console-shim.js` — must compile under ejs and run under + node byte-identically; conservative ES5 by contract (runtime-P3). +- `test/harness-run.js` — 8-line node driver; a copy rides into the esm + generator's transpile dir, so it stays plain CJS. +- `lib/host-config.js.in` — 3-line generated config (has a .d.ts). +- The esprima/escodegen/estraverse/esutils forks — language-P5's + un-forking is the owner. +- The test corpus itself, and `samples/`. + +## Gates + +- typecheck: `tsc -p tsconfig.json` and `tsc -p test --noEmit` clean +- test-eir: the standing 11 compiler-P1.1 pins only, no new reds + (verified same 11 by name against the tsc-converted tree) +- stage0/1/2/3 suites: 424 pass / 21 xfail / 0 fail each +- test-stage1-shapes-off: 424 / 21 / 0 +- test-eir-lowtier: OK +- esm-generation parity: 112/112 runnable byte-identical vs babel-node + +## Notes / follow-ons + +- The stage0 tree is now es2016-level JS (babel's targetless preset-env + downleveled to ES5); node 22 runs both, nothing observed the change. +- compiler-P3 (TS as compiler *input*) is unchanged by this phase and + still coordinates with language-P2 at the parser seam. +- `// generator: esm` is tool-agnostic on purpose: if node's own loader + hooks ever replace the tsc transpile, no directive churn. diff --git a/docs/compiler-p5-results.md b/docs/compiler-p5-results.md new file mode 100644 index 00000000..8c5bd740 --- /dev/null +++ b/docs/compiler-p5-results.md @@ -0,0 +1,127 @@ +# compiler-P5 results — pass configuration: -O suites and -f/-fno- flags (P7.5) + +2026-07-29. The optimizer's configuration surface moves from ~20 +`EJS_*` environment variables to a gcc/clang-style flag surface; env +reverts to what it should be — a short-lived debugging channel +(`EJS_FLAGS`), not the stable interface. + +## What landed + +### The pass registry (`lib/pass-config.ts`) + +One table maps each canonical pass name to its `PassConfig` field, its +default at each -O level, and its help text. `--help`'s pass section +and the `--print-passes` "effective configuration" listing are both +generated from the table, so they cannot drift from the truth. Passes +read the resolved snapshot via `passes()` — never `process.env` (which +under the self-hosted runtime is a rebuild-the-whole-environment +getter; the `SinkFlags` snapshot in optimize.ts that motivated that +rule is now fed from the registry). A side effect worth noting: the +per-instruction env reads in emit.ts (`env_load`/`env_store` inline +slot addressing checked `process.env` on every emitted instruction) +are now plain property reads. + +### -O suites + +- **-O0** — straight lowering: no EIR optimizer, LLVM O0. The + lowering/emission behaviors that were never `opt_level`-gated + (born-shaped, shape guards under `--types`, promote, gc-frames, + inline-alloc, inline-env-slots) stay on at every level — exactly + today's -O0 behavior, preserved deliberately. +- **-O1** — the cheap always-sound intra-function tier: eir-opt, + eir-cleanup, slot-cse, shaped-sink, args-sink, flow-sink. +- **-O2** (default) — adds the module-level tier: devirt, eir-spec, + export-wrapper, ctor-sink, shape-fusion. Byte-identical to the + pre-P5 default pipeline (verified below). +- **-O3** — same EIR suite as -O2; only the LLVM pipeline runs + `default`. The EIR suite and the LLVM level stay one knob, with + `-fllvm-opt=<0..3>` as the escape hatch decoupling the LLVM side. + +### -f/-fno- per-pass flags + +Applied after the suite in command-line order, last-wins (gcc +semantics). Every `EJS_NO_X` maps 1:1 to `-fno-`: + +| old env spelling | new flag | +|---|---| +| EJS_NO_EIR_OPT | -fno-eir-opt | +| EJS_NO_EIR_CLEANUP | -fno-eir-cleanup | +| EJS_NO_SLOT_CSE | -fno-slot-cse | +| EJS_NO_SHAPED_SINK | -fno-shaped-sink | +| EJS_NO_ARGS_SINK | -fno-args-sink | +| EJS_NO_FLOW_SINK | -fno-flow-sink | +| EJS_NO_CTOR_SINK | -fno-ctor-sink | +| EJS_NO_DEVIRT | -fno-devirt | +| EJS_NO_EIR_SPEC | -fno-eir-spec | +| EJS_NO_EXPORT_WRAPPER | -fno-export-wrapper | +| EJS_NO_SHAPE_GUARDS | -fno-shape-guards | +| EJS_NO_POLY_SHAPE_GUARDS | -fno-poly-shape-guards | +| EJS_NO_BORN_SHAPED | -fno-born-shaped | +| EJS_NO_SHAPE_FUSION | -fno-shape-fusion | +| EJS_NO_PROMOTE=a,b | -fno-promote=a,b (and blanket -fno-promote, new) | +| EJS_NO_GC_FRAMES | -fno-gc-frames | +| EJS_NO_INLINE_ALLOC | -fno-inline-alloc | +| EJS_NO_INLINE_ENV_SLOTS | -fno-inline-env-slots | +| EJS_EIR_LOWTIER=1 | -flowtier | + +The env reads are deleted from the passes; the old spellings are inert. +`EJS_FLAGS` (tokenized as extra argv, applied after the real command +line so it wins, restricted to -O/-f tokens) is the single generic env +escape for bisecting inside harnesses that don't thread driver flags. +`test/tester.ts`'s `EJS_EXTRA_FLAGS` (a harness feature that already +threads argv) is unchanged and composes. + +### Consumers ported + +- `lib/eir/tests.ts`: the 12 bisect-flag tests use + `withPassConfig({...}, () => ...)` instead of `process.env` + mutation. +- `buck-test-lowtier.sh` / `//:test-eir-lowtier`: `-flowtier` instead + of `EJS_EIR_LOWTIER=1`. +- CI needed no changes (no workflow set `EJS_*` compile-time vars; + runtime knobs `EJS_GC_*`/`EJS_SHAPES*` are explicitly out of scope — + they configure the produced binary's runtime, not the compile). + +## The A/B gate (before deleting the env reads) + +Run with the env fallback layer still in place (suite → env → flags), +one stage0 binary, comparing `--dump-after eir-opt` output: + +- **env ≡ flag**: 35/35 pairs byte-identical across a targeted corpus + (each pass exercised on files that trigger it; the three emit-level + flags and eir-opt compared at the emitted-.ll level since they don't + show in EIR dumps; lowtier compared on the pre-opt dump; EJS_FLAGS + spelling included). +- **no default drift**: default-config dumps from the HEAD compiler + (built in a worktree) vs this branch — byte-identical for flag-off + and `--types` compiles across the corpus (types-bench2/3/4/5, + modules1, types-flowsink1, types-ctorsink1). +- After deletion: defaults still byte-identical; `EJS_NO_*` verified + inert. + +## Gates + +- tsc typecheck clean. +- test-eir: 216 pass + the same 11 compiler-P1.1 pins, nothing else. +- //:test-eir-lowtier green via `-flowtier`. +- Bootstrap matrix: stage0–3 (incl. the stage2/stage3 fixed point) + + stage1-shapes-off all green, 424 pass / 21 xfail / 0 fail in every + lane — identical to the phase-entry baseline. +- `--print-passes` / `--help` exercised; unknown-pass and bad + `EJS_FLAGS` tokens fail loudly. + +## Decisions and residue + +- **-O1 semantics changed by design**: pre-P5, -O1 ran the full EIR + optimizer (the only gate was `opt_level > 0`); it is now the + intra-function tier. -O2 is bit-for-bit the old default. +- `--types` stays a separate probe flag for now (the open question of + folding it in as `-fmaam` is untouched; it defaults off, so it is + not yet a suite member). +- Tuning knobs that were compile-time constants + (`EJS_SHAPE_FIELD_CAP_MAX` in lower.ts) stay constants — they were + never env vars despite the plan text; `-f=` machinery + exists (`-fllvm-opt`, `-fno-promote=list`) when one needs to become + configurable. +- New capability: blanket `-fno-promote` (the env spelling could only + exclude by substring match). diff --git a/docs/compiler-plan.md b/docs/compiler-plan.md new file mode 100644 index 00000000..a1594185 --- /dev/null +++ b/docs/compiler-plan.md @@ -0,0 +1,213 @@ +# compiler-plan: the EIR middle-end, optimizer residue, the TypeScript port, and driver ergonomics + +Bucket plan; the ordering spine lives in `docs/plans.md` (milestone +references look like `compiler-P1`). Content moved here from the old +plans.md sections "Kill the legacy pipeline", "Optimization phase", +"TypeScript", and "Modules and linking". See `EIRProposal.md` for the +IR design itself. + +## Done: the EIR pipeline (history) + +EIR (the block-argument SSA middle-end in `lib/eir/`) replaced the +AST+intrinsics pipeline outright, in four landed steps: (1) close the +per-function gaps — 424/424 candidate functions, zero fallbacks; (2) +desugars run pre-EIR (classes, destructuring, generators, spread, +meta-properties, hoisting) as pipeline-agnostic AST→AST passes; (3) +toplevel-as-EIR — whole modules lower as one EIR unit; (4) flip the +default and delete — new-cc, LambdaLift, the visitor middle-end and +eleven legacy-only desugars are gone (~9k lines), and a module that +doesn't lower is a compile error. The stage2/stage3 byte-identity +fixed point runs under EIR self-compiles. A pleasant side effect: the +work surfaced 27 latent compiler and runtime bugs, most with +regression tests. + +The optimizer that grew on top (each with its own bucket where large): +guard-region folding/merging + raw f64 joins, function specialization +with a structural escape fence, env scalar replacement, object/array +literal sinking + iterator-wrapper folds (see sinking-plan for the +shaped-world continuation), shape-guard regions (see shapes-plan). + +## Phases + +- [x] **compiler-P1 — Optimizer residue.** DONE 2026-07-28 — + compiler-p1-results.md has the gate numbers. The items from the + original optimization list not owned by sinking-plan or + shapes-plan: + - the usual SSA passes where they pay: constant/copy + propagation, redundant `to_boolean`/`typeof` elimination, + direct-call devirtualization beyond siblings; + - a type lattice over the currently-untyped `any` values, + feeding the low-tier ops beyond what the oracle already + types (TS annotations become a seed once compiler-P3 lands); + - slot-load CSE for toplevel receivers (each module-slot access + currently reloads, which blocks guard-region merging at + toplevel — noted at shapes-P3). + Landed shape (`lib/eir/cleanup.ts`, `lib/eir/devirt.ts`): + - **Type lattice**: trust-free flat lattice over boxed values + (const kinds, fixed-result generic ops, allocation ops, + `add`'s operand rule, block-param meets to fixpoint); sound + with no oracle, so it fires on flag-off compiles too. + - **Cleanup fixpoint** (runs LAST in optimizeFunction, after the + region passes for the same reason foldUnboxOfBox does): + primitive-const folding evaluated in the hosting engine + (string-minting folds and string relationals excluded — + number formatting and collation stay the runtime's; equality + on `-0` declined — the runtime's tag-compare quirk (math2.js + xfail) would otherwise diverge from the host AND break stage + identity under self-compile); typeof folds matching the + RUNTIME's mapping (null→"null" quirk included); + `typeof x === "T"` → the (previously unminted) `typeof_is` + op, now emitted via `_ejs_op_typeof_is_*`; cond_br folding on + known truthiness / never-number `has_tag` / never-shaped + `has_shape`; `to_boolean(logical_not x)` branch inversion; + trivial block-param pruning (the SSA form of copy + propagation); lattice-typed f64 lowering — generic + add/sub/mul/div both of whose operands are proven numbers + compute unboxed with NO guard, and lt/gt feed cond_br via + f64_lt when the whole same-block chain rewrites. + `EJS_NO_EIR_CLEANUP` bisects. + - **Module-slot load CSE** (before the region passes — receiver + identity is what lets toplevel shape regions merge): + block-local availability with store-to-load forwarding, + killed at CALL-effect instructions; plus a dominance tier for + STABLE %self slots (exactly one static store, in the toplevel + entry block — the init-flag-before-body ordering makes the + toplevel run-once, so such a slot never changes during any + activation; accessor setters count as stores, so externally + writable exports never qualify). `EJS_NO_SLOT_CSE` bisects. + - **Devirtualization** (module pass, runs after specialization + so it never starves the strictly-better call_typed rewrite): + SSA-visible `call` of a `make_closure` goes direct with the + closure's env; calls through a single-store %self slot go + direct with an undefined env when the callee's %env param is + unused (load-observes-store proven via the specialize.ts + prefix rule or same-function dominance). Functions whose + closures could reach set_constructor_kind_* decline (the + invoke_closure class-ctor TypeError must survive); an + unenumerable marking operand declines the whole module. + `EJS_NO_DEVIRT` bisects. + - Fallout fixed en route (compiler-p1-results.md has the full + stories): `Map.prototype.delete` was an unimplemented runtime + stub (first compiler-side caller was this phase's CSE); + ejs-llvm lacked every FP IRBuilder binding except createFAdd + (flag-off compiles never emitted f64 before the lattice + pass); the emitter's double-const cache collided `-0` with + `+0` (and the fix's guard had to avoid the strict_eq `-0 === + 0` tag-compare quirk to work under self-host); generator + suspension makes "stable" slots unstable mid-activation — + suspendable functions decline the CSE exemptions. +- [x] **compiler-P1.1 — test-eir debt from flag-off born-shaped + literals.** Found RED at runtime-P4 (P6.3) entry, 2026-07-29: + 11 lib/eir/tests.ts failures, pre-existing (reproduce from + sources untouched by that phase). Three classes: (a) stale + expectations still asserting `make_object keys=[...]` where + flag-off lowering now mints `make_object_shaped` (gc-P5 + part 2), including the "flag-off keeps make_object exactly" + test that asserts the OLD contract; (b) the sinking / + sink-flow fold tests fail knob-independently — the + flow-sensitive sinking does not drain `make_object_shaped` + allocations (real optimizer gap, not just test rot); (c) + `assertNotContains("make_object")` substring-matches + `make_object_shaped`, so those assertions can't distinguish + the two ops. Fix the sinking gap (or decide it's deferred and + assert the shaped alloc form), then repair the expectations + with substring-safe matchers. DONE 2026-07-30. The (b) + diagnosis was wrong — there is NO optimizer gap: both shaped + sinks (`sinkShapedAlloc`, `sinkFlowAllocations`) resolve the + shape through the module's shape table, and the tests' helper + `lowerAndOptimize` discarded the module `lowerOne` returns, so + every shaped candidate silently declined *in the harness + only*. The real pipeline always threads the module + (`optimizeModule` → `optimizeFunction(fn, m, …)`); a stage1 + `--dump-after eir-opt` probe confirmed non-escaping and + written shaped literals drain end-to-end. Fixes (all + tests.ts): `lowerAndOptimize` passes the module; op-exact + matchers `assertContainsOp`/`assertNotContainsOp` (word- + boundary regex — underscore is a word character, so + `\bmake_object\b` rejects `make_object_shaped`); expectations + moved to the born-shaped contract (literal + class-accessor + lowering assert `make_object_shaped shape="…"`, the flag-off + test asserts all-boxed shapes, the escape-materialization test + looks for the materialized shaped op, decline/refusal survival + assertions are op-exact). Gates: test-eir all 227 pass, tsc + clean, stage0-3 + shapes-off 424/21/0 every lane, lowtier OK. +- [x] **compiler-P2 — TypeScript port of the compiler.** The compiler + converts from JS to TypeScript (largely done for lib/eir/ and + lib/*.ts — the strict-TS conversion landed with the EIR work); + remaining: the babel step in `//lib:generated` becomes tsc, and + the residual JS entry points convert. Sequenced before + language-plan work (new-feature work is safer with types + underneath it). DONE 2026-07-29 — docs/compiler-p2-results.md + (one tsc --allowJs pass replaces per-file babel in + //lib:generated; tester.ts + gen-atoms.ts ported strict; + `// generator: babel-node` → `// generator: esm` with + byte-identical baselines; babel removed from package.json/CI; + deliberate JS residue: the harness shim + driver, host-config, + the external-deps forks — language-P5's). +- [ ] **compiler-P3 — TypeScript as compiler input (tentative).** + Slots in at the parser layer (type-stripping or a parser swap, + coordinated with language-P2). TS type annotations then seed + the compiler-P1 type lattice. +- [ ] **compiler-P4 — Modules and linking.** Static linking remains + the regime (no dynamic loading planned): + - reusable native modules from JS: a driver mode compiling a + module to a `.a` plus a generated `.ejs` manifest (exports in + slot order as the ABI, stably-named init function) so + consumers link against compiled modules without recompiling + them; + - IR in the manifest: serialize the module's EIR so cross-module + analysis and inlining through module boundaries work before — + and instead of — any dynamic-loading story. +- [x] **compiler-P5 — Pass-configuration ergonomics: -O suites and + -f/-fno- flags.** DONE 2026-07-29 — + docs/compiler-p5-results.md (registry in lib/pass-config.ts; + suites: -O0 straight lowering, -O1 intra-function tier, -O2 the + full pre-P5 default byte-for-byte, -O3 = -O2 EIR-side with LLVM + default and -fllvm-opt as the escape hatch; every EJS_NO_X + → -fno-x 1:1 (EJS_EIR_LOWTIER → -flowtier), env reads deleted + after a 35-pair env≡flag A/B plus a HEAD-vs-branch default-dump + identity check; tests.ts uses withPassConfig; EJS_FLAGS is the + one env escape; --types stays a separate probe flag). + Original plan follows. Env vars stop being the stable interface for + configuring the optimizer; a gcc/clang-style flag surface + replaces them, and env reverts to what it should be — a + short-lived debugging channel. Current state: `-O0`..`-O3` + exist in the driver but only select the LLVM `default` + pipeline plus one coarse `opt_level > 0` gate on the whole EIR + optimizer; the real per-pass surface is ~20 `EJS_*` vars — the + `EJS_NO_*` bisect family (EIR_CLEANUP, SLOT_CSE, DEVIRT, + EIR_SPEC, SHAPE_GUARDS, POLY_SHAPE_GUARDS, BORN_SHAPED, + SHAPE_FUSION, the `*_SINK` family, PROMOTE, INLINE_ALLOC, + INLINE_ENV_SLOTS, GC_FRAMES), positive opt-ins + (`EJS_EIR_LOWTIER`), and tuning knobs (`EJS_SHAPE_FIELD_CAP_MAX`, + `EJS_SHAPE_NOMATCH`). The shape: + - **pass registry**: one table mapping canonical pass name → + `CompilerOptions` field → default at each -O level; passes + read options, never `process.env` (the per-run flag snapshot + in `lib/eir/optimize.ts` generalizes into this). `--help` + and a `--print-passes` "effective configuration" listing are + generated from the registry so it can't drift. + - **-O suites**: `-O0` = straight lowering (no EIR optimizer, + LLVM O0); `-O1` = the cheap always-sound tier (cleanup + fixpoint, slot CSE, ...); `-O2` = today's full default. + Decide whether `-O3` means anything yet or folds into `-O2`, + and whether the EIR suite and the LLVM opt level stay one + knob (probably yes, with an escape hatch for the LLVM side). + - **-f\ / -fno-\** per-pass overrides, applied + after the suite in command-line order, last-wins — gcc + semantics. Tuning knobs become `-f=`. + - **migration**: each `EJS_NO_X` maps 1:1 to a `-fno-x`; A/B + gate that the old env spelling ≡ the new flag spelling, port + `lib/eir/tests.ts` and the CI lanes off `process.env` + mutation, then delete the env reads from the passes. A + single generic escape (`EJS_FLAGS=` injected as extra argv) + can remain for bisecting inside harnesses that don't thread + driver flags. + - **open questions**: whether `--types` folds in as `-fmaam` + (and eventually defaults on at `-O2`) or stays a separate + probe flag; runtime-behavior knobs (`EJS_GC_*` etc.) are + explicitly out of scope — they configure the produced + binary's runtime, not the compile. + Gates: bootstrap matrix green, stage identity, and the + env≡flag A/B before the env reads are deleted. Independent of + compiler-P2..P4; can land any time. diff --git a/docs/gc-p0-results.md b/docs/gc-p0-results.md new file mode 100644 index 00000000..064519a8 --- /dev/null +++ b/docs/gc-p0-results.md @@ -0,0 +1,162 @@ +# gc-plan Phase 0 — correctness fixes + the measurement numbers + +2026-07-24, M-series macOS (arm64), buck2 + LLVM 22.1.8. All EJS-compiled +user code runs the normal `-O2` opt pipeline (the optimizer-on rule); the +runtime's own optimization level is the experiment's variable (`-O0` as +found → `-O2`, see below). + +## Correctness fixes (all latent-mover-blockers, all real today) + +1. **Collection while executing on a generator stack segfaulted.** + `mark_thread_stack` scanned `[&local, main-stack-bottom)`; on a + generator's malloc'd stack that range spans from the malloc heap + across unmapped memory. Reproduced with `EJS_GC_EVERY_N_ALLOC=7` on + a generator that allocates (signal 11 in `mark_ejsvals_in_range`, + backtrace even showed `_ejs_create_iter_result` — the alloc-after- + `pop_generator` completion path, bug 3 below). Fixed: when the + active-generator chain is non-empty the current-stack scan stops at + the running generator's stack end. +2. **The suspended main-stack segment was never scanned** while a + generator ran. Fixed: each swap-in site records the caller's stack + position (`gen->caller_stack_top`); `mark_generator_stacks` (the + 2015 "XXX mark the actual stack" stub) now roots each ACTIVE + generator object and scans each suspended caller segment up to its + stack's end (main's `stack_bottom` for the outermost, the parent + generator's stack end for nested resumes). +3. **`_ejs_generator_start` allocated the final iter-result AFTER + popping the generator chain** — same bogus-range class as (1) while + still on the generator stack. Fixed by allocating before the pop. +4. **The suspended-generator stack scan was inverted**: it scanned + `[stack_base, saved_SP)` — the DEAD region (stacks grow down) — so + the live frames of every suspended generator were invisible: values + referenced only by a suspended generator's frames could be collected + and resumed-into (use-after-free). Fixed to `[saved_SP, stack_end)` + with out-of-range SPs degrading to a whole-stack scan. +5. **LOS lookups now honor interior pointers** (`find_page_and_cell` + used an exact base match): a large object referenced only through a + derived pointer — likelier once the runtime is `-O2` and base values + die in registers — was collectable out from under the reference. + Interior hits canonicalize to the base (the page-cell path always + did this); the cost is a slightly larger conservative false-positive + surface, which a conservative collector accepts by construction. +6. `_ejs_gc_push_generator` now aborts loudly at MAX_GENERATORS instead + of silently corrupting the chain array. + +Pinned by suite tests `generator23.js` (GC while running on the +generator stack), `generator24.js` (suspended-frame-only liveness across +forced collections), `generator25.js` (nested active chain) — all +node-identical, all green under `EJS_GC_EVERY_N_ALLOC=7`. + +**Pinned, NOT fixed (pre-existing, outside gc scope):** an uncaught +exception thrown out of a generator body aborts the process (the +desugar's outer catch rethrows on the generator stack and the unwinder +walks off the makecontext frame; node prints the exception in the +caller). Recorded here so the exceptions/coroutine interaction gets an +owner later. + +## Instrumentation (EJS_GC_PROFILE=1) + +Two bits from the header's gc-reserved range (57-63; every existing +consumer masks): YOUNG — set at allocation, cleared on first survival, +so "young" = allocated since the last collection, exactly a nursery's +population; PINNED — set once per cycle per object hit by a +conservative reference (recorded even when already marked: the white +check is a marking optimization, not a pin filter). Per-cycle stderr +line: live set, young-allocated vs young-survived (count/bytes/%), pins +by source (cstack / regs / genstack) with env-interior, LOS, young/old +splits, pause. Shutdown summary (atexit): totals, rates, kind and +size-class histograms. The YOUNG-bit OR is folded into the header +store the allocator already does; everything else is behind the env +var — the measured path is unperturbed when profiling is off. + +## The numbers, runtime `-O0` (as found) + +**Self-compile** (stage1 `ejs.exe` compiling `ejs-es6.js`, the real +workload; 127.4s wall): + +- **79.5M allocations, 3,419MB** — 26.8MB/s, 624K allocs/s. +- Kinds: **object 47.5M (60%), closureenv 31.0M (39%)**, primstr 0.94M, + primsym 14. The env-churn hypothesis is confirmed: 2 of every 5 + allocations are closure environments — gc-P2's inline `make_env` + fast path targets the right thing. +- Sizes: ≤32B: 31.4M / 957MB; ≤64B: 42.9M / 2,022MB; ≤128B: 5.2M / + 440MB; **LOS: 2,489 / 0.86MB** — the heap is uniformly tiny-object. +- Survival: warmup cycles 33%/35%/12.5%, then **steady-state 2.5-3.5% + of young bytes survive** each ~60-110MB cycle — a nursery reclaims + ~97% of its space per minor GC on the compiler workload. +- **Pins: 100-650 objects (5-36KB) per cycle** out of ~1M-object live + sets — C-stack source dominates, registers contribute 1-5, + generator-stack 0 (none active), **env-interior 0**, LOS 0. The pin + population is 4-5 orders of magnitude smaller than the live set. +- Pauses: 240-770ms per cycle; total 9.2s = 7.2% of wall. + +**Kernels** (types-bench2 2.00s / types-bench3 0.31s under `--types`, +matching their P4.x records): 1.57M allocs per 60MB cycle, **young +survival 0.0-0.1%**, pins 13 objects, pauses ~21ms. + +**Generator kernel** (gens1small): allocations on the generator stack, +chain pins visible under stress; profile attributes genstack pins once +generators are suspended with live frames. + +## What the numbers decide (the plan's open orderings) + +- **gc-P2 (nursery + inline alloc) is GO, and P3 need NOT move ahead of + it**: the pin rate under pure conservative roots is trivially small + (≤650 objects/cycle, KBs), so premature-promotion erosion from pinned + young objects is negligible. Bartlett cell-pinning at this rate is + free; precise JS frames (gc-P3) remain a throughput/paranoia + improvement, not a prerequisite. +- **Inline allocation should cover `make_env` AND plain objects** + early: objects+envs are 99% of allocations. +- The LOS is irrelevant to the mover's economics today (2,489 allocs, + <1MB) — the P4.2 slot-cap workaround (field cap 14) stays until the + planned size-class/lookup work, with no added urgency from these + numbers. +- Marking cost (not sweep) dominates the pause at `-O0`; the `-O2` + runtime move (below) and later concurrent marking (gc-P6) both attack + it. + +## The `-O2` runtime experiment — LANDED + +Runtime moved `-O0` → `-O2` (`defs.bzl`), scanner assumptions +re-verified: `MARK_REGISTERS` spills callee-saved registers explicitly +(volatile asm), live-across-call values sit in callee-saved registers +or caller frames per the ABI (both scanned), interior pointers +canonicalize in page and (now) LOS lookups, and the generator stress +tests exercise collection from generator stacks under optimization +(generator23-25 + EJS_GC_EVERY_N_ALLOC=7 all green on the -O2 build). + +**Results:** + +- **Self-compile: 127.4s → 41.6s wall (3.06×).** The allocation totals + are bit-identical between the runs (79,529,117 allocs / 3,419.54MB — + the workload is deterministic, which doubles as an instrumentation + sanity check). Total pause 9.2s → 6.05s (240-770ms → 140-608ms per + cycle); survival and pin profiles unchanged (steady-state 2.4-3.4% + young-byte survival; pins 380-530 objects/cycle, cstack-sourced, + `regs` drops to 0 — the optimized runtime holds fewer stray ejsvals + in callee-saved registers at the collection point; env-interior + still 0). +- **types-bench2 (--types): 2.00s → 0.68s (2.9×)** — most of what P4.5 + recorded as the "1.71s allocation-loop residual" was runtime `-O0` + overhead, not intrinsic allocation cost. types-bench3: 0.31s → + 0.18s. (Flag-off bench2: 6.7s → measured on the -O2 runtime at the + gate as well.) +- The `-O2` flip is kept (defs.bzl comment records the P0 verification); + gc-P2's inline-allocation gate ("strictly better than the free-list + path") must be measured against THIS baseline. + +Measurement gotchas recorded for future phases: the EIR optimizer sinks +non-escaping allocations, so a churn kernel can profile as ~zero allocs +(size the probe's escapes deliberately — the optimizer-on rule cuts +both ways); survived-bytes are cell-size-accounted while allocated-bytes +are request-accounted, so tiny survivor sets can read as >100% on +sub-KB cycles (harmless at real scales). + +## Phase checklist impact + +- gc-plan P0: DONE (this doc). P1's remaining scope: forwarding + helpers only (the 64-bit header + reserved bits + lib/types.ts + lockstep landed with shapes P4.1). +- gc-P2 proceeds with conservative roots; P3 stays sequenced after + (pin-rate evidence above). diff --git a/docs/gc-p2-results.md b/docs/gc-p2-results.md new file mode 100644 index 00000000..bedb27ee --- /dev/null +++ b/docs/gc-p2-results.md @@ -0,0 +1,141 @@ +# gc-P2 results: generational nursery + emitted allocation/barrier seam + +Completed 2026-07-25. Nursery is ON by default; `EJS_GC_NURSERY=off` +selects the old collector (the A/B knob the differential lane uses). + +## What shipped + +- **P2a — slot-based Scan protocol.** `EJSValueFunc` takes `ejsval*`; every + precise scan (roots, modules, remset, transitive) can rewrite slots. + 42 call sites converted; property maps walked directly + (`scan_property_entries`). +- **P2b — nursery + evacuating minor GC.** One dedicated 32 MB arena + (`is_young` = range check); size-class bump pages (young=1, allocated-ness + = below-bump rule) and survivor pages (young=2, bitmap rule); conservative + cell pinning (C stacks + registers + every live generator stack); + evacuation via first-word forwarding (gc-P1 bits); promotion into old + free-list pages; 1 MB default minor budget (`EJS_GC_NURSERY_BUDGET`). +- **Object-remembering write barrier** (second design; the slot-address + remset was abandoned after dangling recorded slots in freed/realloc'd + malloc storage proved unfixable by enumeration): `_ejs_gc_remember(owner, + value)` — inline filter (traceable, value-young, owner-not-young, + DIRTY-bit dedup) then owner append; minors re-Scan dirty owners against + whatever storage they own *at scan time*; pinned-young referents re-dirty + the owner (edge carry); LOS objects are born dirty; full GC prunes the + buffer. +- **P2c — emitted seam.** `EJSHeapContext _ejs_heap` exported ([12 × i64]: + bump[5], limit[5], nursery_base, nursery_end — append-only layout + contract); emitted inline `make_env` allocation (bump/compare/init/box, + slow call = safepoint; `EJS_NO_INLINE_ALLOC` bisect); emitted store + barriers at env/slot stores (inline young-check reading the seam words, + out-of-line `_ejs_gc_remember_val`); shaped-object stores remember the + slot-array env (the storage owner), not the wrapper object. +- **Conservative-lookup bounds prefilter** (`conservative_lo/hi`, widened + at `arena_new` and `alloc_from_los`, checked in the stack scanners and at + the top of `find_page_and_cell`). Not nursery-specific — it fixed two + pathologies (below) and speeds the old collector's full marks as well. + +## The bug that nearly killed the phase + +The compiler self-compile under `EJS_GC_NURSERY=1 EJS_GC_EVERY_N_ALLOC=101` +crashed in module init with a 0xa7-poisoned receiver while *every* checker +(barrier-coverage verify, whole-heap paranoid walk, module-slot death +detector, sweep-time reverse-referrer lookup over old gen + LOS + roots + +modules + the raw C stack) stayed green. + +Root cause: **unrooted `ejsval` C statics in the ejs-llvm native bindings** +— chiefly `_ejs_StructType_prototype` (never `_ejs_gc_add_root`ed). The +prototype was live and correctly *evacuated* (reachable via +`ctor.prototype`; every scanned copy rewritten), but the C static kept the +stale nursery address, and `StructType_impl` births every subsequent +wrapper object with a dead proto. The non-moving collector never noticed: +liveness was sufficient and addresses were eternal. + +The mover lesson, stated once: **roots exist to rewrite locations, not just +to keep referents alive.** Any C-side `ejsval` that outlives a collection +and is later read must be registered as a root (or re-derived from a +scanned location on every use). Fix: all unrooted statics across the 17 +ejs-llvm binding files rooted (prototype statics *and* constructor statics +— the latter had an init-time window between `_ejs_function_new` and the +exports `setprop` read). + +Debug tooling built for the hunt (permanent, all in `runtime/ejs-gc.c`): + +- reentrant-minor / young-alloc-during-minor / page-install-during-minor + aborts; minor-end seam-desync check; sweeping-an-active-page check; +- `EJS_GC_PARANOID` sweep-time reverse-referrer lookup (names every + location still referencing a dying young cell); +- `EJS_GC_WATCH=` cell-lifecycle tracer (alloc / pin / evacuate / + sweep-poison, each with a C backtrace) — the tool that named the killer; +- per-phase minor timing (`phases[pins/roots/dirty/wl/sweep]`) and a + full-GC phase line, both under `EJS_GC_PROFILE`. + +## The two performance pathologies + +First honest interleaved timing said nursery-ON self-compile was **2.4× +slower** (43.4 s → 103 s). Phase profiling attributed 60.5 s of the 65 s +minor total to the conservative pin phase, and 13.1 s of a single full +collection (54 MB live!) to `process_worklist`. One cause, two faces: + +1. **Stack scan**: every C-stack word paid an arena bsearch and, on miss, + a locked linear LOS-list walk. Deep compiler recursion × a growing + heap made minors cost 25→230 ms. With the bounds prefilter in the + scanners: pins 60.5 s → 0.3 s (p50 25 ms → 2 ms). +2. **Full-mark edges**: references to *static atoms* (which live outside + every arena) fell through to the same locked LOS walk on every edge — + and nursery mode had never culled the LOS list (no full GCs), so it was + thousands of entries long: ~13 µs per marked object. With the + prefilter at the top of `find_page_and_cell`: worklist 13.07 s → 45 ms, + the full collection 13.2 s → **59.6 ms**. The old collector's full + marks chase the same atom edges — it got faster too (self-compile 43.4 s + → 39.3 s with the nursery *off*). + +## Numbers (final build, interleaved ×3, arm64 M-series) + +| workload | old collector | nursery (default 1 MB) | +|---|---|---| +| self-compile (full pipeline) | 38.7–40.0 s | 39.0–39.2 s (4 MB budget: 39.1 s) | +| types-bench2 --types | 0.69–0.70 s | 0.64 s | +| envbench1 | 1.81–1.96 s | 1.34–1.48 s | + +GC work on the self-compile: old = 34 stop-the-world collections, +9.3 s total pause (worst ~1.3 s); nursery = ~1060 minors totaling ~4.6 s +(p50 2.1 ms at 4 MB budget) + one 60 ms full collection. + +Minor pause distribution (envbench corpus) by budget: + +| budget | minors | p50 | p99 | envbench wall | self-compile wall | +|---|---|---|---|---|---| +| 512 KB | 1221 | 0.48 ms | **0.68 ms** | 1.35 s | 43.4 s | +| 1 MB (default) | 611 | 0.93 ms | 1.27 ms | **1.34 s** | 40.4 s | +| 2 MB | 306 | 1.99 ms | 2.57 ms | 1.42 s | — | +| 4 MB | 153 | 4.08 ms | 5.27 ms | 1.48 s | **39.1 s** | + +The <1 ms p99 gate is met at the 512 KB setting; the 1 MB default trades +p99 1.27 ms for ~7 % better self-compile throughput. Self-compile minors +run heavier than the bench corpus (p99 ~54 ms at 4 MB budget — deep stacks +and promotion bursts). + +Pin report (self-compile): conservative pins ~330 objects/minor (KBs); +full-GC census: cstack 167 objects / 8 KB, registers 0, generator stacks 0. +Pinning remains 4–5 orders of magnitude below the live set — the P0 +conclusion stands. + +## Validation + +- probes (ropes1/2, envwb1, gens1small/2small, gennest, genstress1/2): + 8/8 byte-identical across off / on / stress-101 / stress-101+verify. +- nursery-diff lane: every suite test compiled once, run off vs on vs + stress-997, byte-compared — 475 pass / 0 fail / 1 n-a (tester.js). +- self-compile: nursery, nursery+stress-997, nursery+stress-101 (tiny), + paranoid and verify lanes on the gennest ladder — all green. +- matrix ×7 green (final build). + +## Measurement caveats + +- emitted inline allocations are invisible to `EJS_GC_PROFILE` alloc + counters (they never enter `_ejs_gc_alloc`); +- heap addresses are only stable across runs under lldb (no ASLR) — + `EJS_GC_WATCH` targets must come from the same-process run; +- differential-lane exes statically link the runtime: after any runtime + change, `rm test/*.exe` or the lane silently tests the old collector code. diff --git a/docs/gc-p3-results.md b/docs/gc-p3-results.md new file mode 100644 index 00000000..b2b671f7 --- /dev/null +++ b/docs/gc-p3-results.md @@ -0,0 +1,103 @@ +# gc-P3 results: precise JS-frame roots (emitter gc-frames) + +Completed 2026-07-25. The chained-frame variant of gc-plan P3: emitted +functions own their precise root records; frame-held values relocate. + +## What shipped + +- **The chain.** `EJSHeapContext.gc_frame_head` (seam word 17; the + emitted view widened `[12 x i64]` → `[18 x i64]`). An emitted + function whose values are live across a safepoint allocas an + `EJSGCFrame { prev, count, slots[count] }`, links it in its prologue, + unlinks at every return. Catch handlers re-link their own frame (the + unwind discarded every callee record). Chains are **per machine + stack**: the generator push/pop hooks swap the head exactly like + `current_stack_end` (caller segment parks on the generator), and the + minor walks the live chain, every suspended generator's saved chain, + and every active generator's parked caller segment. +- **Slot demotion, not spill/reload.** A value live across a safepoint + is demoted: stored to its frame slot at its definition, and **loaded + at every use** (`val()` intercepts). Every load is dominated by the + def's store, so there is no dominance hazard from rewriting SSA uses + across branches; LLVM CSEs redundant loads between safepoints and + cannot forward across one — the frame address escapes through the + chain link, which is exactly the store-to-load-forwarding discipline + the plan demanded. Slots are undefined-initialized (a stale slot + must parse as an ejsval). +- **Liveness** (`lib/eir/liveness.ts`): standard backward analysis over + EIR; safepoints = target-less ops with GC|CALL effects (`box_f64` + excluded — it never allocates). Deliberately partial, and sound + because of one load-bearing ABI fact: **a live-across-call SSA value + is always in a callee-saved register or a stack slot, so the + conservative scan sees it and pins its referent.** Under-coverage + costs pins, never correctness. v1 skips invoke-form safepoints (try + regions) and values defined by them — those stay pinned. +- **Env slot-address inlining.** `env_load`/`env_store` compute the + slot address inline (payload mask + `+16 + 8*slot`), recomputed per + use from the boxed env value — a relocated env re-derives through its + own slot load. Deletes a runtime call from every env access. + Bisects: `EJS_NO_GC_FRAMES`, `EJS_NO_INLINE_ENV_SLOTS`. +- **Pin-first ordering.** The chain walk runs AFTER the conservative + pin pass, on purpose: an object visible to both a gc-frame slot and a + C frame (an ejsval argument into the very call that triggered the + minor) must not move — `minor_process_slot` leaves pinned targets in + place, so the pin wins and the C copy stays valid. Precise-first + would have been a use-after-move factory. + +## The bug measurement caught + +First profile: `gcframe_moves = 0` across 4256 minors, pins UP 6×. +The gc-frame is a stack alloca — **the conservative stack scan saw +every frame slot and pinned every frame-held value through its own +slot.** Precision existed but could never move anything. + +Fix: during a minor, `mark_ejsvals_in_range` skips the frame records of +the stack it is scanning (`set_frame_skip_chain` — a sorted range list +with a merge cursor, O(1) per word). Each conservative range scan gets +its matching chain: the live head for the current stack, the saved head +for a suspended generator stack, the parked caller segment for each +active generator. Full collections never skip — the old collector +still relies on conservative slot visibility (it doesn't move, so it +doesn't need to rewrite). + +## Numbers (self-compile, arm64, nursery default-on, 1 MB budget) + +- **Movement**: `gcframe_moves` = 76,174 relocations per self-compile + (p50 13/minor, max 101) — the "move-everything" property runs + continuously; under `EJS_GC_EVERY_N_ALLOC=101` stress every + frame-held young value relocates constantly, which is the + store-forwarding trap the gate demanded (green across the ladder). +- **Pins**: p50 373 → **101** per minor (mean 452 → 136) after the + skip fix; ~45% fewer pin events per compile than the P2 baseline. + Residual pins = C-frame-referenced values + stale dead spills — the + set precision cannot touch, as predicted. +- **Spill cost** (variant exes, one self-compile each): P2 baseline + 42.3 s; env-inlining alone 41.9 s; frames alone 43.3 s; both 42.7 s. + Net ≈ **+1% wall** — frames cost ~1 s, env inlining gives back + ~0.4 s. (The plan's "expected small: safepoints are call sites; + calls spill anyway.") + +## Validation + +- tiny ×3 / gennest compile + runs / paranoid / verify, all under + `EJS_GC_EVERY_N_ALLOC=101`; self-compile under stress-997 — green. +- probes (ropes, envwb, gens, gennest, genstress): 8/8 byte-identical + across off / on / stress / stress+verify. +- nursery differential lane (full recompile — gc-frames are in ALL + emitted code): 475 pass / 0 fail / 1 n-a. +- --types differential lane: 485 identical / 0 divergent / 1 n-a. +- matrix ×7 green. + +## Deferred (recorded) + +- Invoke-form safepoints (try regions) and their results stay + conservatively pinned; covering them needs reload placement on the + normal edge (single-pred case is easy; shared continuations need + edge splitting). +- Slot liveness is per-value, not interval-packed; frame sizes are + small in practice. +- Return-address-keyed stackmaps (the zero-entry-cost upgrade) remain + the measured-later variant; chain maintenance cost is within noise. +- Dead-slot floating garbage: a slot keeps its last value alive until + the frame pops (bounded by frame size; undefined-init bounds it at + function entry). diff --git a/docs/gc-p4-results.md b/docs/gc-p4-results.md new file mode 100644 index 00000000..0e58ad8b --- /dev/null +++ b/docs/gc-p4-results.md @@ -0,0 +1,154 @@ +# gc-P4 results — mostly-copying major compaction + the pin-scan cliff fix + +Phase P6.1 (plans.md) / gc-P4 (gc-plan.md). Three deliverables, one +commit: the conservative pin-scan cliff fix (the plan's "first order of +business"), the mostly-copying major compaction, and the auto-tuned +growth target. + +## 1. The pin-scan cliff fix (arena reservation + O(1) lookup) + +The mechanism (evidence recorded in gc-plan.md §gc-P4 while gating +sinking-P3): each arena was its own mmap, so once a late arena landed +beyond the C/LLVM heap the conservative prefilter span +`[conservative_lo, conservative_hi)` swallowed the malloc heap, and +every stack word pointing into LLVM's own allocations passed the +prefilter into a per-word arena bsearch (and formerly a locked linear +LOS walk). Self-compile wall time was bistable per run (6s-vs-60s, +mmap-layout luck) and quasi-deterministic per binary. + +The fix, in `runtime/ejs-gc.c`: + +- **One arena reservation at init**: `MAX_HEAP_SIZE` (2GB) of address + space, `ARENA_SIZE`-aligned, mapped `PROT_NONE` and committed one + 32MB arena at a time (`arena_space_reserve` / `arena_new` via + `mprotect`). The arena span is fixed and disjoint from the C heap + for the life of the process — nothing foreign can ever be mapped + inside it — and the linux boxability hint (sub-2^47) is applied once + at reservation time. +- **O(1) arena lookup**: `(ptr - arena_space) >> ARENA_SHIFT` into a + direct map (`arena_lookup`) replaces the per-word bsearch. + `heap_arenas[]` stays for iteration and is address-sorted by + construction (sequential carving). +- **LOS sorted-range array**: `los_ranges` (binary search, grow/remove + on alloc/free) replaces the locked linear walk of `los_list` for + conservative candidates; the `[los_lo, los_hi)` stopgap bounds from + sinking-P3 remain as the quick reject. +- Drive-by: `release_to_los` now unmaps the whole mapping (header + + bitmap slop), not just `alloc_size` — the old code leaked the tail + page of every freed large object. + +## 2. Mostly-copying major compaction + +After a full collection's sweep, `compact_old_gen` evacuates the live +UNPINNED cells of the sparsest pages of each size class into the free +space of denser pages, rewrites every reference through the gc-P1 +forwarding records, and returns the emptied pages to their arenas. + +- **Pinning**: the conservative mark helpers now set the PINNED header + bit (bit 58) on every hit during a full collection — under + `EJS_GC_PROFILE` this rides the existing `profile_note_pin` dedup. + Every registered generator also pins (the registry is an intrusive + list of raw pointers, and the generator's own address is baked into + its `makecontext` args). Pinned cells sweep in place; pins clear in + the fixup walk. +- **Selection**: per size class, pages sorted live-count-ascending; the + COMPLETE source set is chosen before any evacuation (a source must + fit in the pool that excludes it and all prior sources). The + one-pass version had a real bug the frag benchmark caught: an early + DESTINATION could later be selected as a source via its stale live + count, evacuating more cells than the accounting reserved + ("compaction ran out of destination space" abort). +- **Fixup surface**: root-set slots (includes every shape's rooted + `name`), module Scan, gc-frame chains (no-ops today — frame-held + referents are conservatively pinned — walked for future-proofing), + remset entries (raw owner pointers), every live heap cell via + `old_gen_walk` + young survivor pages (Scan slots, primstr raw + children, self-interior pointers via `minor_fixup_evacuated` at copy + time). Sources skip via the FORWARDED header bit; freed afterwards + with no finalizers (the objects live on). +- **Safety facts established** (why moving OLD objects is sound): + property maps content-hash names; symbol hashcodes are cached + in-object; WeakMap/WeakSet ride hidden properties on the key; Map/Set + are linear SameValue lists; shapes transition tables key on shape + indices + content hashes; the only raw-pointer webs into the heap are + the generator registry (pinned) and rope/dependent string children + (fixed up). +- `EJS_GC_COMPACT=off` restores plain mark-sweep for A/B; the young + survivor-page-emptied-by-full-sweep path got a latent list-corruption + fix on the way (`young_page_freed`: the page lives on + `heap_priv.young_pages`, but `_ejs_finalize_obj` detached it from the + `heap_pages` bucket list, silently unlinking neighbors and leaving a + stale young-list head). + +### Shrink gate (frag benchmark, `GC.heapSize()` added for the gate) + +400k 3-slot objects, keep every 16th, clobber the stack, collect twice: + +| | heap after collect | +|---|---| +| compact **on** | **8.28 MB** (moved 38,177 objs, freed 7,159 pages) | +| compact off | 37.61 MB | + +4.5× shrink, identical checksums, second collect moves 0 (idempotent). +Nursery-off variant: 2.44 MB vs 38.44 MB. + +Test-writing lesson (cost an hour): garbage "dropped" at module +toplevel is conservatively retained by stale stack slots of the +toplevel frame — 7 pins held 800k objects transitively. Allocate in a +callee and clobber the stack before measuring. + +### Stress gates + +- `//:test-eir` + `//:test-stage1` green with compaction default-on. +- gc-genstress1 / generator23-25 / frag under `EJS_GC_NURSERY=off + EJS_GC_EVERY_N_ALLOC=997` (a compacting full GC every 997 allocs): + byte-identical output compact-on vs compact-off. +- Full self-compile under `EJS_GC_NURSERY=off` (compaction exercised on + every trigger for the entire compile): completes, produced compiler + runs. + +## 3. Auto-tuned growth target (knob census = 1) + +`full_gc_trigger()` replaces the duplicated `60MB` constant at both +trigger sites: a full collection fires when old-gen growth since the +last one exceeds `EJS_GC_GROWTH` percent (default 50) of the post-sweep +footprint, floored at two arenas (64MB — the old constant's cadence for +small heaps). With compaction shrinking the footprint, the trigger now +adapts in BOTH directions. `EJS_GC_GROWTH` is the census's one knob. + +## Timing (self-compile A/B, arm64, same tree, same probe) + +- **Baseline (HEAD runtime, per-arena mmaps)**: this binary sat in the + cliff's DEEP slow mode — it never completed one self-compile inside a + 10-minute timeout (attempt 1), and attempt 2's first run took ≈10 + minutes (inferred from process start times; the kill ate the buffered + probe output). `sample` during the run: ~95% of stacks inside + `_ejs_gc_minor_collect → mark_ejsvals_in_range → find_page_and_cell`. + This is the sinking-P3-era evidence reproduced at full strength — the + slow mode is a property of the BINARY's allocation layout, and this + binary drew the short straw. +- **Fixed (arena reservation + direct map + LOS bsearch)**: 62.6s / + 63.4s / 64.6s / 64.7s across 4 runs — the bistability is gone. A + profiled run: 56s wall, 106M allocs/4.65GB, GC total ≈ 5.5s (~10% of + wall: 4.45s across 5,723 minors, max minor pause 10.9ms — down from + the 500-860ms cliff pauses; 1.06s across 3 fulls). Compaction on the + real workload: the three full GCs freed 759 / 5,375 / 5,666 pages + (~2.9 / 21 / 22 MB returned per collection). + +So the fix is worth ~10× on unlucky binaries and removes the layout +lottery entirely; the residual GC share of a healthy self-compile is +~10%, of which pin scans are no longer the dominant term. + +## Follow-ups / deferred + +- Full-GC remset rooting retains dead dirty owners (`mark_object_root` + on every remset entry) and the post-sweep rebuild keeps them — a + self-sustaining garbage-retention cycle observed at 65536-entry + overflow in the frag test's first draft. Scanning dirty owners' + young edges without marking the owner live (or filtering dead owners + first) would fix it; not this phase's scope. +- Arena decommit: emptied arenas stay committed (page-level reuse only); + `mprotect(PROT_NONE)`/`madvise` on fully-free arenas is a cheap + follow-up now that the reservation exists. +- LOS is never compacted (by design) and `calc_heap_size` still counts + only page bytes, not LOS. diff --git a/docs/gc-p5-results.md b/docs/gc-p5-results.md new file mode 100644 index 00000000..dad68477 --- /dev/null +++ b/docs/gc-p5-results.md @@ -0,0 +1,140 @@ +# gc-P5 results: shapes intersection — single-cell shaped objects + +Completed 2026-07-28. The Step B design addendum lives in +gc-plan.md (the gc-P5 bullet); this doc records what shipped and the +gate numbers. + +## What shipped + +- **Single-cell shaped objects (embedded slots).** Born-with-shape + allocation places the slot storage inside the object's own cell: + 32B object + 16B embedded closureenv header + slots, one GC cell. + The governing design choice: `obj->slots` remains a closureenv-boxed + ejsval that merely points at `obj+32`, so the compiled slot + addressing seam (`slotRef`), has_shape guards, the verifier + contract, and the C storage engine's accessors are all UNTOUCHED — + embedded-ness is pointer identity (`env == obj+32`), no new header + bit, and growth past birth capacity silently degrades to the old + out-of-line array. Entry points: + - `_ejs_object_new_shaped` derives the true shape FIRST (pure, + transition-memo'd, ~one compare per field), then births object + + storage as one cell; anything off-script falls back byte-for-byte. + - Constructor results: a **birth-capacity hint on EJSFunction** + (one-shot feedback — the first construct's field count sizes every + later `this`). Ordinary `Construct` allocates `this` with + embedded capacity = hint. Works flag-off; zero compiler plumbing. +- **Barrier owner flip.** Shaped-slot stores now remember the wrapper + OBJECT (all five C sites + emitted `slot_store`); the ordinary Scan + walks slot values directly in both storage modes and scans the env + edge only when out-of-line. Remset owners are therefore always cell + heads — the interior-pointer entry class never exists. Scan order + (values, then edge) is load-bearing: a dirty rescan must rewrite + value slots before a young out-of-line env is evacuated. +- **Evacuation.** Whole-cell memcpy (the existing routine); the + embedded slots ejsval joins `minor_fixup_evacuated`'s + self-interior-pointer cases (flat strings, EJSArguments) and is + never presented to the precise slot callbacks (they assume + object-base payloads). +- **Per-shape trace masks (typed-slot trace elision).** `EJSShape` + grows `f64_mask`, built incrementally at intern time (parent mask | + edge bit). The shaped Scan skips f64 slots — precise trace elision + for raw doubles — on every collector walk (mark, minor, compaction + fixup, paranoid/verify) since they all route through the specop. + Barrier elision for typed stores was already true and is now + documented: emitted f64 `slot_store` skips the barrier + (emit.ts), and the runtime filter exits on non-traceable values. +- **Born-shaped literals go flag-off (lower.ts).** The + `make_object_shaped` literal lowering drops its oracle gate: key + order/count are the site's static truth; without the oracle the + static reprs are all-boxed and the runtime's birth derivation + supplies true ones. Flag-off literals now allocate single-cell and + are eligible for the shaped-literal sinking. Flag-off semantics are + identical by construction (`make_object` was `object_create` + + per-key setprop — exactly `new_shaped`'s screens and fallback). +- **The 256-byte size class is enabled.** `ffs(256)=9 > + HIGH_LIMIT_BITS` had routed 256B cells to the LOS since the + beginning — the nursery seam, bump arrays, and emitter mapping were + already built for 5 classes. With gc-P4's LOS bsearch + direct + arena map in, the class is on: `HEAP_PAGELISTS_COUNT` +1, three ffs + threshold comparisons +1, emitter inline-env cap 128→256. Every + cap-14 shape now fits a single cell (`EJS_SHAPE_EMBED_FIELD_MAX = + EJS_SHAPE_FIELD_CAP_MAX`), and 15..30-slot envs take pages, not the + LOS. + +## Numbers (arm64 M-series, medians of 3) + +| workload | before (d48cf69) | after | +|---|---|---| +| types-bench2, flag-off | 2.47–2.53 s | 2.37 s | +| types-bench2, --types | 0.21 s | 0.21 s | +| litbench1 (escaping-literal loop, flag-off) | 1.56 s | **0.85 s (1.84×)** | +| self-compile (stage2 action wall) | 62–64 s recorded (gc-P4) | 60–67 s across runs — parity | + +Allocation shape, types-bench2 flag-off: object+env cells **8.0M → +4.0M** (the 4M separate slot arrays are gone), requested bytes 305 → +244 MB, closureenv count 4,000,061 → 42. + +Allocation shape, compiling lib/desugar.js (the compiler compiling a +real module, flag-off): closureenv 5.47M → 5.22M; **LOS allocations +85,304 → 26,251 (−69%)** with 83,549 now in 256-byte page cells. +Shape-table transitions are UNCHANGED (~11.3M) — the born-shaped +derivation still walks one memo edge per field; what changed is cells, +bytes, and the per-add call path. + +**The headline correction this phase records**: shapes-P5's +"types-bench2 residual 1.71s = the allocation loop, gc-P5's half" is +obsolete. The ctor-sinking phases (sinking-P2/P3) virtualized both +bench2 construct sites (`ctorSunk=2`), and --types bench2 is now +0.21 s on the phase-entry baseline already. Profiling shows the old +"alloc loop" time was predominantly guard-miss generic property +traffic plus pre-sink allocation — gc-P5's real payoff is flag-off +code, literal-allocating loops, heap footprint, and LOS pressure. + +## Measured and deferred (the shapes-P6 discipline) + +- **Emitted bump allocation for `make_object_shaped`**: sampling the + 1.84×-improved litbench puts `_ejs_object_new_shaped` + `gc_alloc` + at ~6% of in-process samples; generic property reads (strict_eq, + getprop) dominate the flag-off residual. The emitted-inline variant + is deferred on that evidence. Design note for whoever picks it up: + inline stamping of the STATIC shape diverges from the runtime's + true-repr derivation (flag-off claims are all-boxed; a number stored + later would repr-flip the shape per object) — either derive + number-ness inline per boxed-claimed field or revisit + `classify_repr`'s number→f64 policy first. +- **Emitted inline construct-result allocation**: unnecessary — the + EJSFunction hint gets constructor results single-cell with no + compiler involvement, and the epoch-guarded ctor sink already + deletes the allocation entirely where it matters under --types. + +## Gates + +- Matrix ×7 green at every step (test-eir, lowtier, stage0–3 including + the stage2/stage3 byte-identity fixed point, stage1-shapes-off). +- Embedded-slot stress probe (growth past capacity, ctor hints with + under-sized hints, dictionary migration out of embedded storage, + repr flips, old→young stores through existing slots, enumeration + order, `in`): node-identical under + EJS_GC_EVERY_N_ALLOC=7/31/101, EJS_GC_PARANOID=1, + EJS_GC_NURSERY=off, EJS_SHAPES=off, EJS_GC_COMPACT=off. +- types-typedslots1 / types-bornshape1 / types-poly1 (--types builds) + green under the same stress envs (poly1 A/B'd bit-identical against + the phase-entry baseline binary). +- --types diff lane at phase close: **475 files, 474 identical, 0 + divergent, 1 N/A** (tester.js, the standing esprima parse gap) — + LANE PASS. + +## Notes for later phases + +- The old collector's promotion allocator + (`old_alloc_cell_for_promotion`) showed 61 samples walking its + free-page list in the compile profile — a P6.3-refactor-adjacent + perf item. +- Self-compile in-process time is dominated by `_ejs_op_strict_eq` + (196 samples — property-name compares in generic get paths and Map + lookups) and string flatten/compare churn, not allocation: the next + self-compile win lives in flag-off property access (compiler-P1 + lattice territory), not the collector. +- The pre-existing gc-P4 note about remset-rooted dead dirty owners + self-sustaining across full GCs applies unchanged to the new + object-owner entries. diff --git a/docs/gc-plan.md b/docs/gc-plan.md new file mode 100644 index 00000000..d0ba8631 --- /dev/null +++ b/docs/gc-plan.md @@ -0,0 +1,785 @@ +# GC plan: an industrial generational moving collector, co-designed with the compiler + +Phase ids here are `gc-P0`..`gc-P7` (formerly bare P0..P7 in this +doc). The ordering spine lives in `docs/plans.md`. + + +A plan for replacing echojs's stop-the-world conservative mark-and-sweep +collector (`runtime/ejs-gc.c`) with a generational, moving, eventually- +concurrent collector — in independently-landable phases, each of which leaves +the tree green and shippable. + +**The stance, up front.** An earlier revision of this document took the current +repo state — conservative stack scanning, runtime-call allocation, the hash-map +object model — as fixed, and designed a collector around *tolerating* it +(Bartlett mostly-copying with pervasive pinning). That got the substrate right +and the ambition wrong. The compiler is ours and is being actively rebuilt +(EIR, the optimizer, the maam type oracle); every allocation site, every store, +and every safepoint in compiled code is an EIR op we control, declared in the +effect table (`lib/eir/ops.ts`: `E.GC`, `E.WRITE`). A modern collector for this +engine is a **compiler/runtime co-design**: precise, relocatable roots in JS +frames because we emit them; inline allocation because we emit that too; +barriers the optimizer can elide; object layout designed once, jointly with the +maam shapes work. Conservatism survives only where it is genuinely stuck — the +hand-written C runtime — and the mostly-copying substrate exists to absorb +exactly that remainder, not to excuse imprecision everywhere. + +**Why now.** The compiler work is landing: EIR optimization (literal sinking, +IIFE inlining, env scalar replacement, DCE), typed guarded arithmetic (10.3× on +a numeric kernel, maam-plan P3), with specialization (P3.6) and shapes (P4) +queued. As mutator time falls, allocation and collection become the floor. The +goal of this plan is that **GC is never the reason echojs loses a benchmark**: +allocation as cheap as a bump-and-compare, minor pauses sub-millisecond, major +pauses bounded, and a design that scales with the object-model improvements +rather than fighting them. + +## What we have today, as found + +- **Collector** (`runtime/ejs-gc.c`, ~1700 lines): stop-the-world, + single-threaded, tri-color mark-and-sweep, non-moving. Trigger is 60 MB of + allocation since the last cycle (`ejs-gc.c:1408`), plus `GC.collect()`, + allocation-failure fallbacks, and shutdown. +- **Allocator**: segregated free-lists in size classes 16–256 bytes over 32 MB + arenas, a per-page bump pointer for fresh pages, and a large-object store + (LOS) for anything larger. Every allocation — including from compiled JS — + is a call to `_ejs_gc_alloc(size, scan_type)` (`ejs-gc.h:33`). +- **The heap is already precisely traceable.** Every object carries a per-class + `Scan` spec-op (`ejs-object.h`) plus typed scanners for strings, symbols, and + closure environments. The collector knows the exact outgoing edges of every + heap object. This is the single most important asset we have. +- **Roots are not precise.** An explicit root list (~142 registrations, almost + all static singletons), module exotics, and **a conservative scan of the C + stack and spilled registers** (`mark_thread_stack`, `MARK_REGISTERS`) that + treats anything pointer-shaped — including interior pointers — as a root. +- **Generator stacks are not scanned at all** — `mark_generator_stacks` is a + stub (`ejs-gc.c:1087`). A latent correctness bug today; a hard blocker for + any mover. +- **Value representation**: SpiderMonkey-style NaN-boxing (`runtime/ejsval.h`); + GC pointers live in the low 47 bits of an 8-byte value, heap addresses forced + below 2⁴⁷. +- **Compiler emits no GC support.** The EIR backend (`lib/eir/emit.ts`) keeps + locals as pure SSA values — "locals never touch memory"; no statepoints, + stackmaps, or safepoint metadata. Notably, `env_load`/`env_store` each make a + runtime call to `_ejs_closureenv_get_slot_ref` and then load/store through + the returned raw `ejsval*` (`emit.ts:619-643`) — an interior pointer the + conservative scanner must honor, and a per-access call the mutator pays. +- **No write barriers anywhere.** No card table, no remembered set, no handle + abstraction in the C runtime. +- **Single-threaded.** One event loop, no workers, GC lock macros are no-ops. + Collections only happen inside `_ejs_gc_alloc`, i.e. under a runtime call. +- **Build**: Buck2, Homebrew LLVM **22.1.8**, runtime compiled **`-O0`** + (`defs.bzl:83`, inherited from the old config.mk), user JS compiled `-O2`. + New collector code goes in `runtime/BUCK` `shared_sources` and must survive + compilation as Objective-C on macOS. + +## Assets, liabilities, and the resulting shape + +Three assets determine the design: + +1. **Precise heap tracing already exists** (the `Scan` ops). Evacuation is + "copy + rewrite the slot" wherever marking today is "gray the target." +2. **The mutator is single-threaded — today.** Barriers need no atomics, + safepointing is one handshake, and a collector thread (later) coordinates + with exactly one partner. We exploit this deliberately — but **concurrent + JS is a stated goal** (Workers; the tc39 shared-memory work), so every + single-mutator shortcut is taken behind a seam with a documented exit path + (§"Concurrent JS"). The structural insight that keeps this cheap: the + platform's first concurrency step is *isolates* — N heaps, each with one + mutator — which preserves per-heap single-mutator simplicity; only + shared-memory objects ever put two mutators in one space. +3. **We own every emitted allocation, store, and safepoint.** The effect table + already classifies them (`E.GC`, `E.WRITE`). Anything the design needs from + compiled code — spill slots, barriers, inline allocation, liveness metadata + — is an emitter feature, not a research project. + +One liability: **root precision**, and it splits cleanly in two: + +- **JS frames** — ours to fix. The emitter will make them precise *and + relocatable* (see "Roots" below). This is the plan of record, not a fallback. +- **C runtime frames** — hundreds of `EJS_NATIVE_FUNC`s holding bare `ejsval` + locals across allocation points. Making these precise is the SpiderMonkey + exact-rooting migration (years). We don't do it: C frames stay conservatively + scanned, and objects they reference get **pinned** for the cycle. JSC ships + this way permanently; pinning C-frame referents is industrially respectable, + and every collection necessarily has some C frames live (the alloc slow path + is C), so the pin population never reaches zero anyway. What matters is that + it becomes *small and bounded* once JS frames are precise. + +The **mostly-copying substrate** (Bartlett) is what lets both root regimes +coexist in a moving collector: precise references are evacuated and rewritten; +ambiguous references pin their targets in place for the cycle. In the earlier +revision pinning had to absorb *all* stack roots; here it absorbs only the +C-runtime remainder — but the machinery is identical, and it means precision +work is an incremental improvement, never a flag-day prerequisite. + +NaN-boxing stays. It forecloses LLVM's native statepoint relocation (which +needs reference-typed values), but not precision — see below. The oracle-driven +hybrid representation (typed values as real `ptr addrspace(1)`, maam-plan +P3.6's typed calling convention taken further) remains the far-future path to +native statepoints for the typed fraction of the program; nothing in this plan +blocks it and nothing waits for it. + +## Target architecture + +``` + ┌────────── nursery ──────────┐ ┌─────────── old gen ───────────┐ + inline ──→ │ bump pointer, block chain; │ │ block-structured; evacuated/ │ + bump alloc │ evacuating minor GC; │ ─→ │ compacted per-block; pinned │ + in JS code │ pins at cell granularity │ │ cells swept in place │ + └─────────────────────────────┘ └───────────────────────────────┘ + ┌──── LOS ────┐ + size > threshold ──→ │ mmap'd, never moved │ + └─────────────────────┘ + + roots: JS frames — precise, relocatable (emitter-owned gc-frame slots) + C frames — conservative scan → cell-granularity pinning + barrier: card marks + SATB old-value log, one barrier, two consumers; + compiler elides barriers on initializing stores + later: concurrent marking on a collector thread; brief STW evacuation; + optionally fully concurrent evacuation (Brooks forwarding) + further: Workers = N isolates (one heap+mutator each, this design ×N); + tc39 shared structs = a contained shared space, atomic + protocols scoped to it alone +``` + +## Roots: precise, relocatable JS frames + +### What LLVM offers (condensed; the conclusions matter) + +- **`llvm.gcroot`** wants pointer-typed slots; an ejsval is an `i64` that is + only sometimes a pointer. Unusable — the abandoned experiment in + `lib/abi.ts:34-46` hit exactly this. +- **`gc.statepoint` + `RewriteStatepointsForGC`** relocates GC values LLVM can + *type* as GC pointers (`ptr addrspace(1)`). Incompatible with polymorphic + NaN-boxed i64s; reachable only after a value-representation split (the P3.6 + hybrid). Far future, not load-bearing. +- **`llvm.experimental.stackmap`** records the *locations* (register, stack + slot, or Direct alloca) of arbitrary-typed live values — including i64 — at + a given call site, into an `__LLVM_StackMaps` section keyed by return + address. It records; it does not relocate. + +### Plan of record: emitter-owned gc-frame slots + +The emitter gives each function a **gc-frame**: a contiguous alloca array of +ejsval slots. At every safepoint (every `E.GC`-effect op — all of which lower +to calls, including the inline-allocation slow path): + +1. every live GC-typed value is **stored** into a gc-frame slot before the + call, and +2. every use after the call reads the **reloaded** value — the emitter rewrites + the SSA uses, so no pre-safepoint copy survives the call. + +Because the roots now live in memory we own, the collector can *rewrite* them: +precise **and relocatable**, NaN-boxing intact, no LLVM fork. This is the +"shadow stack" idea, but built where it belongs — in our own emitter, on the +liveness information EIR already has. + +How the collector *finds* the frames, two variants, decided by measurement: + +- **Chained frames (start here).** Function prologue links its gc-frame record + (base, slot count) onto a thread-global chain; epilogue and unwind edges + unlink it. Simple, portable, no binary-format work. Costs a couple of stores + per function entry/exit — measurable, possibly ignorable, and functions the + optimizer proves allocation-free (no `E.GC` ops transitively) skip the frame + entirely. +- **Return-address-keyed maps (the zero-entry-cost upgrade).** Emit + `llvm.experimental.stackmap` at each safepoint listing the gc-frame slots; + the collector walks frame pointers and looks up return addresses in the + `__LLVM_StackMaps` section. No per-call chain maintenance; the stackmap + intrinsic serves purely as a *metadata emitter* while the slots themselves + make relocation sound. Requires a stack walker + section parsing (Mach-O and + ELF) and `-fno-omit-frame-pointer` discipline. + +Two sharp details, named now because they are the kind that silently corrupt: + +- **Store-to-load forwarding.** If LLVM can prove the safepoint call doesn't + touch the gc-frame, it will forward the pre-call store to the post-call load + and the collector's rewrite is lost. The gc-frame base must **escape** (the + chain registration does this naturally; under the stackmap variant, escape it + explicitly once per function). Verify with a stress test that moves *every* + object *every* collection. +- **Interior pointers must not be live across safepoints.** `env_load`/ + `env_store` currently materialize raw `ejsval*` slot refs via a runtime call. + The emitter should instead compute slot addresses inline (a GEP off the env + base) and **recompute per use** rather than caching across a safepoint — the + env base is then the only rooted value, and it relocates like any other. + Bonus, independent of GC: this deletes a runtime call from every env access, + a straight mutator win available today. + +The C runtime keeps the existing conservative scanner verbatim — stack ranges, +`MARK_REGISTERS`, interior-pointer canonicalization via `find_page_and_cell` — +but its hits **pin** rather than mark. No handle API, no rewrite of hundreds of +native functions. + +**Sequencing note.** The collector does not *wait* for precise JS frames: the +mostly-copying substrate runs with fully conservative roots on day one (that's +the earlier revision's design, still sound), and precision lands as a +pin-rate reduction. Phase 0 measures where the pins actually come from; if the +young-gen pin rate under conservative roots is already low, precision can slide +later in the sequence with no design change. + +## Allocation: inline the fast path + +Every allocation today is a full call into `-O0` runtime code. Industrial +engines allocate in ~4 inline instructions; so will we: + +``` +bump = *bump_ptr; new = bump + size; +if (new > *limit) goto slow; // slow: call _ejs_gc_alloc_slow → safepoint +*bump_ptr = new; // object header init follows inline +``` + +Single-threaded means the bump pointer is a plain global — no TLS. The +emitter stages this by allocation kind, payoff-ordered: + +1. **Closure environments** (`make_env`) — the most frequent allocation in + closure-heavy code, trivial to initialize inline (header + length), size + known at compile time. +2. **Object/array literals** (`make_object`/`make_array`) — worth inlining + once shapes land and initialization is "store shape id + slots" rather + than "build a hash map". +3. Strings/others stay runtime-side. + +The slow path is the safepoint; the fast path never GCs, which is what makes +"spill live values at safepoints only" cheap — straight-line allocating code +pays nothing. + +**Pretenuring**: the maam oracle (or cheap runtime feedback) tags allocation +sites whose objects reliably survive; those sites' inline sequence bumps an +old-gen block instead. Small change once the generational split exists. + +**Interplay with allocation sinking.** The optimizer is already removing +allocations (env scalar replacement landed; object/array sinking planned in +`docs/plans.md`). These compose — sinking removes allocations, the nursery +makes the survivors cheap — but they must be *measured together*: Phase 0's +allocation profiling runs with the optimizer on, so both efforts see the same +numbers and neither claims the other's wins. + +## Write barrier: one barrier, two consumers + +Generational collection needs old→young stores caught; concurrent marking +(later) needs overwritten values logged. Build one barrier that does both from +day one: + +- **Card marking** for location: old gen divided into ~512 B cards; a store + into old gen dirties the card (shift + byte store, unconditional, no + branches). Minor GC scans dirty cards only. +- **SATB old-value log** for the future concurrent marker: the barrier records + the overwritten ejsval into a sequential-store buffer. Dormant until Phase 6, + but designing it in now is what makes concurrency an *addition* rather than a + barrier rewrite. +- **Non-atomic everything** — single mutator. A plain store to the card byte, + a plain SSB append. Revisit only if Workers ever land. + +Where it goes — the store surface is small and enumerable: + +- **Runtime**: `_ejs_object_setprop` and the property-map insert path; one + barrier covers most object writes. +- **Emitted code**: `env_store` and `module_slot_store` are the only inline + ejsval stores (`emit.ts`); the emitter adds the card-dirty sequence there. + +And where it *doesn't* go — the compiler elides barriers it can prove dead: + +- **Initializing stores.** Stores that fill in a just-allocated object + (literal construction, `make_env` slot init) target an object that is + necessarily nursery-resident: no barrier. This is the majority of stores in + allocation-heavy code and the elision is purely local. +- **Provably-young targets.** The optimizer/oracle can extend "just allocated" + to "allocated in this function and not yet escaped/collected-across". +- **Non-reference stores.** Once typed slots exist (f64 slots in specialized + envs/shapes, post-P3.6/P4), stores of unboxed doubles need no barrier and no + trace entry at all. + +## Object header, forwarding, and shapes (joint design with maam P4) + +`GCObjectHeader` is a bare `uint32_t` (`ejs-types.h:30`). **Widen it to 64 +bits** with room for: forwarded bit + forwarding address (or the classic +first-word overwrite — objects are 8-aligned, low bits free), age, pin, mark, +card/log bits, and — the important part — a **shape/trace-map index**. + +Layout mechanics: for `EJSObject` the widening is free (4 B of padding already +follows the header before the `ops` pointer — compiled code's view via +`lib/types.ts` doesn't shift). `EJSClosureEnv` and `EJSPrimString` shift their +second word; runtime structs and `lib/types.ts` must move in **one atomic +change**, verified with the old collector still active. + +**The shapes tie-in is the single highest-leverage item in this document.** +maam-plan P4 designs shape-guarded property access from the oracle's +`layouts()` (per-allocation-site field names, offsets, type sigs). That design +and the GC's object layout are **one design, written once**: + +- objects become **shape id + contiguous inline slots** — fixed-size, trivially + copyable, no out-of-line malloc'd `_EJSPropertyMap` (which today never + compacts and is traced through a virtual call); +- tracing becomes a **per-shape pointer-offset bitmap** — branch-free, no + indirect `Scan` call, and exactly what a fast evacuation loop wants; +- objects are **born with their shape** at oracle-known allocation sites — no + dynamic hash-map buildup; +- property storage lives **in the GC heap** and compacts with everything else; +- and it is the doorstep to inline caches, which is where "competitive with + V8" actually gets decided. + +This plan's P1 header change reserves the bits; the shapes design doc (maam +P4) fills them in. The GC must not ship a header layout that shapes then has +to break. [Update 2026-07-23: that design now exists — **docs/shapes-plan.md** +— written against this section's layout; its Step A claims 24 bits + a mode +bit of the widened header for the shape index, and its P4.1 lands jointly +with this plan's P1 as the one atomic layout change, whichever starts first.] + +Per-kind moving notes: `EJSObject` copies as a struct (the property map, while +it still exists, is malloc'd and stays put); envs copy header+slots with each +slot rewritten; flat strings copy, out-of-line buffers stay put, ropes' +children are ordinary edges; LOS never moves; suspended generator stacks are +conservative root ranges (pin) once the Phase-0 bug fix lands. + +## Concurrency I: the collector on its own thread + +The mutator stays single-threaded (per isolate — see the next section); the +collector eventually gets its own thread, in two tiers: + +1. **Concurrent marking + brief STW evacuation — the sweet spot; target + first.** Marking (the long phase, proportional to live set) runs on the + collector thread, fed by the SATB log; a single-mutator handshake takes the + root snapshot; a short STW window evacuates unpinned survivors (small, + because generational). Pause becomes proportional to survivors, not live + set. SpiderMonkey lived here productively for years. +2. **Fully concurrent evacuation.** Objects move while the mutator runs; needs + a per-object forwarding word (Brooks — the widened header has room) and a + load barrier. Real cost per load; adopt only if tier 1's numbers demand it. + An *incremental* (time-sliced, same-thread) marker is the cheaper fallback + if the collector thread proves troublesome — a rung, not a requirement. + +Conservative/pinned roots compose fine with both tiers: pinned objects simply +don't move, and precise JS frames (relocatable) are what allow +stack-referenced objects to participate in evacuation at all. + +## Concurrency II: concurrent JS — Workers, shared memory, multiple mutators + +Concurrent JS is a goal, not a hazard: the web platform has Workers, and tc39 +is converging on safe shared-memory primitives (SharedArrayBuffer today; +shared structs / `Atomics` extensions in progress). The design must +accommodate multiple concurrent mutators *eventually* without paying for them +*now*. The platform's own staging makes that tractable, because each step +isolates a different cost: + +1. **Workers as isolates — N heaps, one mutator each.** The web's Worker model + shares nothing traced: `postMessage` copies, and a `SharedArrayBuffer` is + untraced off-heap memory (the GC only keeps the per-isolate wrapper object + alive — SAB support is *easy* and can come early). Under isolates, + everything in this plan holds per-heap unchanged: non-atomic barriers, + one-handshake safepoints, a lock-free bump nursery — each isolate has its + own. What isolates require is that collector state be **instantiable**: + `ejs-gc.c` today is a pile of file-static globals. New collector code puts + all state in a heap-context struct from day one, so "spin up a second + isolate" is plumbing, not a rewrite. + +2. **Compiled-code contact points go through a seam.** Emitted code touches + heap state at a handful of named points: bump/limit pointers, card-table + base, gc-frame chain head, (later) a safepoint-poll flag. The emitter + treats these as **context accessors** — today they resolve to plain + globals; under isolates they become TLS loads or a pinned context register. + Because echojs is AOT and statically linked, flipping the accessor + implementation is one emitter change plus a world recompile — there is no + deployed-binary ABI to preserve. The discipline that matters now is *not + scattering* heap-state contact through emitted code, so the flip never + grows a long tail. + +3. **Shared-memory objects are the real multi-mutator step — contained in a + shared space.** When shared structs (or an engine-level shared heap) land, + shared objects live in a distinct **shared space** with the expensive + protocols scoped to it alone: atomic card/SATB barriers on stores into + shared objects, CAS-installed forwarding if it ever moves (more likely: + non-moving initially), collection under a global rendezvous of all + isolates. The tc39 proposal's own containment rule — shared objects + reference only other shared data — is exactly what keeps this tractable: + isolate→shared edges are roots into the shared space; shared→isolate edges + don't exist by construction. Per-isolate nurseries and old gens keep their + cheap single-mutator protocols forever. + +4. **Safepoint reachability.** Allocation-slow-path safepoints suffice for one + mutator. A multi-isolate rendezvous needs every thread to reach a safepoint + promptly, including one spinning in a non-allocating loop — that means + emitter-inserted **back-edge polls** (a flag check; EIR knows its loop + back-edges). Not emitted today; reserved as a known emitter feature, and + the gc-frame design already gives polls a place to stand. + +What we do **now** (cheap, structural): +- no new file-static collector state — everything in the heap-context struct; +- heap-state access from emitted code only via the context-accessor seam; +- metadata designed atomics-friendly: side mark bitmaps that can be set with + an atomic OR, a forwarding word that can be CAS-installed, card/SATB buffers + that shard per-thread; +- no protocol that is correct *only* for one mutator by construction — the + single-mutator fast paths must be the degenerate case of a design that + admits N, not a different design. + +What we do **not** do now: no locks or atomics on any hot path, no shared +space, no rendezvous protocol. Those are paid when the platform work arrives, +and the seams above are what make the bill small. + +## Knobs + +One primary knob: a **heap-growth target** — collect when live × (1 + g) is +reached, `g` auto-tuned from recent survival rates. Nursery size, block size, +card size, promotion age: derived, not exposed. Existing `EJS_GC_*` env vars +survive as debug overrides only. If a knob can be derived from a measurement, +derive it. + +## Adjacent runtime work (same bottleneck, not this collector) + +Named here because "the runtime is about to be the bottleneck" is bigger than +GC, and these are cheap: + +- **The runtime is compiled `-O0`** (`defs.bzl:83`). Moving to `-O2` is likely + the single cheapest runtime speedup available and directly speeds the + collector itself. The conservative scanner's assumptions (register spills, + no hidden pointer representations) must be re-verified under `-O2` — do it + in Phase 0 while instrumentation is fresh. LTO across runtime/user-code is a + further step with the same caveat. *(DONE at P0: `-O2` landed after + verification — self-compile 3.06× faster, types-bench2 2.9×; see + gc-p0-results.md. LTO remains open.)* +- **`env_load`/`env_store` runtime-call round-trip** — inline the slot address + computation (also required for precise roots; see above). Can land early and + alone. +- **Property access cost** (hash map, no ICs) — owned by shapes (maam P4), not + this plan; noted so nobody aims the GC at a mutator problem. + +## Phased plan + +Bias, as with the eir/maam plans: small phases, matrix green after each +(`//:test-eir`, `//:test-stage0..3`, the `--types` diff lane once relevant), +each independently revertable. The old collector stays behind a build flag +through Phase 3 for A/B and differential testing. + +- **gc-P0 — Correctness prerequisites + measurement.** Fix generator stack + scanning (`ejs-gc.c:1087` stub) — a real bug today, a corruption source under + any mover. Add instrumentation: allocation rate and size/kind profile (with + the optimizer on), survival rates, and a **pin-rate estimator** — walk + conservative roots and report pinned bytes, retained-block counts, and pin + *sources* (C stack vs. register spill vs. env interior pointers), separately + for what would be young vs. old. Run the `-O2`-runtime experiment and + re-verify scanner assumptions. **Gate: the numbers.** They size the payoff of + every later phase and decide how early precise JS frames need to land. + **DONE 2026-07-24 — docs/gc-p0-results.md.** The generator work found + FOUR bugs (crash on collect-during-generator-execution; unscanned + suspended main segment; alloc-after-pop on completion; and the + suspended-stack scan bounds INVERTED — it scanned the dead region and + missed every live frame), pinned by generator23-25 under gc-stress; + LOS lookups made interior-tolerant. The numbers: 2.4-3.4% steady + young survival, 39% closureenv allocation share, pins in the hundreds + of objects/KBs per cycle (⇒ P2 ships on conservative roots; P3 stays + behind it), and the `-O2` runtime landed at 3.06× on the self-compile. + +- **gc-P1 — Header widening + forwarding plumbing.** 64-bit header, bits + reserved per the shapes tie-in; coordinated `runtime/` + `lib/types.ts` + layout change, landed atomically with the old collector active; forwarding + read/write helpers. No behavior change. **Gate: matrix green on all three + bootstrap targets.** + +- **gc-P2 — Generational nursery: the payoff phase.** Block-structured + spaces; all new collector state in an instantiable heap-context struct and + all emitted heap-state access through the context-accessor seam + (§"Concurrency II" — this is when the discipline starts, because this is + when the new code is written); bump-pointer nursery with the **inline + allocation fast path** for + `make_env` (objects follow later); card-table + SATB-logging store barrier + (runtime sites + the two emitted sites, with initializing-store elision); + **evacuating minor GC** on the mostly-copying substrate — precise heap edges + and root-list entries evacuate, conservative hits pin at cell granularity + (`find_page_and_cell` already canonicalizes interior pointers). Old gen + stays mark-sweep. **Gate: allocation throughput strictly better than the + free-list path; minor-pause p99 sub-millisecond on the benchmark corpus; + differential vs. old collector across the whole suite plus a + collect-every-N-allocations stress mode; pin-rate report from real runs.** + +- **gc-P3 — Precise JS-frame roots.** Emitter-owned gc-frame slots at `E.GC` + safepoints with SSA-use rewriting; chained-frame variant first; env slot + address inlining (interior pointers die); allocation-free functions carry no + frame. Nursery pins drop to C-frame-referenced objects only. **Gate: + move-everything stress mode green (catches store-forwarding bugs); pin rate + vs. Phase 2 recorded; mutator regression from spills measured and + acceptable; matrix green.** + +- **gc-P4 — Mostly-copying major collection.** Evacuate/compact unpinned + old-gen blocks; pinned cells swept in place; heap actually shrinks. This is + where fragmentation dies. **Gate: identical output vs. Phase 3 under stress; + demonstrated heap shrink on a fragmenting benchmark; auto-tuned growth + target replaces the 60 MB constant, knob census = 1.** + + **FIRST ORDER OF BUSINESS (measured 2026-07-25, while gating + sinking-P3): the conservative pin scan has a scaling cliff that + dominates self-compile wall time.** Evidence, so it isn't + re-derived: on the desugar.js-closure compile (20 modules), minor-GC + pin scans total 41–126 s of a 46–132 s wall — pauses grow from + <1 ms early to 500–860 ms during deep-recursion parse/lower phases. + Mechanism: `mark_ejsvals_in_range` treats every stack word as a raw + pointer candidate; the only rejection before the per-word arena + bsearch (and, before the sinking-P3-era fix, a LOCKED LINEAR walk of + the whole LOS list) is the `[conservative_lo, conservative_hi)` span + — and once a late arena or LOS mmap lands beyond the C/LLVM heap, + that span swallows it, so during codegen MILLIONS of stack words + pointing into LLVM's own allocations pass the prefilter. The cost + is therefore bistable per RUN (mmap layout luck: the same binary + compiles the same input in 6 s or 60 s) and quasi-deterministic per + BINARY (any allocation-pattern change — sinking-P3's was +1.4% + allocs — shifts when arenas are minted and can lock a binary into + the slow mode; its stage1 sat at ~1.5–2× baseline wall). A + bounds prefilter for the LOS walk (`los_lo/los_hi`, + ejs-gc.c) landed with sinking-P3; the real fixes belong here: + reserve arena address space once at init (span stays tight and + disjoint from the C heap forever, and arena lookup becomes two + compares + an index instead of a bsearch), and give the LOS a real + lookup structure (the P6.3 refactor). Self-compile wall time should + then sit at the fast mode (~6 s for the desugar closure) + deterministically — a bigger win than most optimizer phases. + +- **gc-P5 — Shapes intersection (floats with maam P4).** When the shapes + design lands, the collector consumes it: per-shape trace bitmaps replace + `scan_type` + virtual `Scan`; inline-slot objects copy as memcpy + bitmap + walk; property storage moves into the GC heap; inline allocation extends to + object literals; typed slots get barrier/trace elision. Sequenced by + maam-plan; the GC-side work is deliberately small because P1 reserved the + header bits. + + **Settled design (2026-07-28, the Step B addendum).** The governing + choice: shaped slot storage stays a *closureenv-shaped* region reached + through the `obj->slots` ejsval — but born-with-shape allocation places + it **inside the object's own cell** (object header, ops, proto, slots + ejsval pointing at `obj+32`, then an embedded env header + the slot + values). Embedded-ness is pointer identity (`env == (char*)obj + + sizeof(EJSObject)`), no new header bit. Because the compiled + slot-addressing seam (`slotRef`, emit.ts) already loads the slots + ejsval and indexes the env, **compiled slot access, has_shape guards, + and the verifier's contract change not at all**; only allocation sites + and the collector know. Consequences, each independently gated: + - **Single-cell shaped allocation**: `_ejs_object_new_shaped` grows a + shape-index-passing form (trusting the module's interned shape, + values verified against the f64 mask, fallback = today's + re-derivation); one cell of `32 + 16 + 8n` bytes replaces the + object-cell + env-cell pair. Constructor allocations get there via + a **birth-capacity hint on EJSFunction** (set from the result's + field count after the first construct; ordinary Construct allocates + `this` with embedded capacity = hint) — no compiler plumbing, works + flag-off. Growth past embedded capacity falls back to an + out-of-line closureenv (today's doubling path); the object stays + shaped, the embedded region goes dead. + - **Barrier owner flip**: shaped-slot stores remember the *wrapper + object* (C sites and emitted slot_store both; today they remember + the env), and the ordinary object's Scan walks the slot *values* + directly in both modes (plus the env edge only when out-of-line). + This makes owner pointers always cell heads — no interior-pointer + remset entries — and dirty-object rescans see embedded slots. + - **Evacuation**: whole-cell memcpy (the existing routine) + a shaped + case in `minor_fixup_evacuated`'s self-interior-pointer fixup (the + flat-string/EJSArguments precedent): rebase the slots ejsval when + it points into the moved cell. The embedded slots edge is never + presented to the precise slot callbacks (they assume object-base + payloads); Scan's mode switch owns that. + - **Per-shape trace masks**: the shape record gains an f64 bitmap + (u16, built incrementally at intern time from parent | repr); the + ordinary-object walk skips f64 slots — precise trace elision — and + the three hot collector sites (mark, minor trace, compact fixup) + may short-circuit `ops->Scan` for `_ejs_Object_specops` objects + into the same inline walk. Out-of-line arrays keep the closureenv + range scan (raw doubles are NaN-box-valid numbers; unchanged). + - **The 256-byte size class is enabled**: `ffs(256)=9 > + OBJECT_SIZE_HIGH_LIMIT_BITS` routes 256B cells to the LOS today — + an off-by-one that predates gc-P4's LOS lookup fix and the direct + arena map. Enabling the already-plumbed class (pagelist, seam + words, emitter cap all exist) makes every cap-14 shaped object + single-cell (`32+16+112 = 160 ≤ 256`) and takes >14-slot envs off + the LOS; A/B-measured at the gate (frag bench + self-compile). + - **Emitter inline allocation for `make_object_shaped`** (literals): + the make_env bump-sequence precedent, one guard (module shape + global != NOMATCH — literal installs are CreateDataProperty, so no + epoch/proto check is needed), header stamped with the shape index, + initializing stores, no barriers. Inline `fill_object_shaped` is + measured-later work (the ctor hint already single-cells it). + - **Typed-slot barrier elision is already true** (emitted f64 + slot_store skips the barrier; the runtime filter exits on + non-traceable values) — the phase audits and documents it; the new + elision is the trace mask above. + +- **gc-P6 — Concurrent marking + STW survivor evacuation.** Collector + thread, single-mutator handshake, SATB log becomes live. **Gate: marking off + the mutator; STW time independent of live-set size; stress-differential + green.** + +- **gc-P7 — Fully concurrent evacuation (optional).** Brooks forwarding + + load barrier, only if Phase 6's pause numbers say so. + +Phases 0–4 deliver the generational mover with no threads and no value-rep +change; 5 fuses the collector with the object-model future; 6–7 buy pause +bounds as needed. + +## Risks, named + +- **Pin rate before precision lands.** Phases 2 runs with conservative roots; + if the young-gen pin rate is high (plausible: env interior pointers, `-O2` + register pressure), premature promotion erodes the win until Phase 3. Phase + 0 measures this *first*; if it's bad, Phase 3 moves ahead of Phase 2's gate + being declared, or ships together with it. Cell-granularity pinning bounds + the damage to pinned bytes either way. +- **Store-to-load forwarding across safepoints** (Phase 3) — the silent- + corruption class. Mitigated by the escape discipline and killed dead by the + move-everything stress mode, which must exist before the first relocating + root does. +- **The header widening is a coordinated cross-language change.** Runtime + structs and `lib/types.ts` move together or compiled code reads garbage. + Atomic land, old collector active, all three bootstrap targets. +- **Spill overhead at safepoints** (Phase 3). Live-across-call values get + stores/reloads LLVM might otherwise have kept in callee-saved registers. + Expected small (safepoints are call sites; calls spill anyway); measured at + the Phase 3 gate, and allocation-free functions opt out entirely. +- **`-O2` runtime and scanner assumptions.** The conservative scanner was + hardened against `-O0`-runtime/`-O2`-mutator asymmetry; flipping the runtime + to `-O2` re-opens those assumptions. Phase 0 owns re-verifying them. +- **Objective-C compilation on macOS** — collector sources compile as ObjC in + `runtime/BUCK`; keep them clean C. +- **Single-mutator shortcuts leaking past their seams.** Non-atomic barriers, + the global bump pointer, and one-handshake safepoints are deliberate + exploitations of today's engine — but concurrent JS is a goal, so each must + stay behind the §"Concurrency II" seams (heap-context struct, context + accessors, atomics-friendly metadata). The cheap discipline is refusing new + file-static collector state; the expensive mistake would be a barrier or + forwarding protocol that is single-mutator-only *by construction*. + +## Alternatives considered + +- **Keep mark-sweep, add a non-moving generational layer.** Cheaper, gets + minor-pause wins, no compaction ever — fragmentation and cache locality stay + bad, and the shapes future wants copyable objects. The Phase 2 substrate + costs only modestly more; not worth the dead end. +- **Full exact rooting including the C runtime (handles everywhere).** + The SpiderMonkey migration; years of churn across hundreds of native + functions for a pin population that pinning already bounds. No. +- **Native LLVM statepoints now.** Requires un-NaN-boxing or the P3.6 hybrid + value representation. The gc-frame design gets relocatable precision without + it; statepoints remain the far-future upgrade for typed values, and nothing + here blocks that. +- **MMTk (or another off-the-shelf collector).** The serious outside option; + its binding model fits AOT runtimes. But it wants exactly the root precision + and barrier plumbing this plan builds anyway, and adopting it forfeits reuse + of the existing precise `Scan` ops and battle-tested conservative scanner. + Reconsider if the hand-rolled collector stalls at Phase 4+; the compiler-side + work (roots, barriers, inline alloc) transfers either way. + +## Coordination with maam-plan / plans.md + +- **maam P3.6 (specialization)**: raw f64s in registers/typed signatures are + invisible to GC (not listed in gc-frames) — correct by construction. Typed + slots later enable barrier/trace elision. +- **maam P4 (shapes)**: joint design of header bits, trace bitmaps, inline + slots, born-with-shape allocation (this plan's Phase 5). The P4 design doc + should be written against the Phase 1 header layout. +- **Oracle pretenuring**: allocation-site lifetimes → old-gen birth; consumes + Phase 2 infrastructure. +- **plans.md escape analysis / allocation sinking**: measured jointly with + Phase 0's allocation profile; sinking shrinks nursery pressure, the nursery + cheapens what remains. +- **IR-in-manifest (cross-module)**: whole-program oracle facts strengthen + pretenuring and barrier elision; no GC dependency. + +## Phase checklist (for /goal sessions) + +- [x] **gc-P0** generator-stack fix; alloc/survival/pin instrumentation (optimizer + on); `-O2`-runtime experiment + scanner re-verification. + *Gate:* matrix green; numbers recorded in this doc or a results doc. + DONE 2026-07-24 — docs/gc-p0-results.md has the numbers. Headlines: + four latent generator-scan bugs fixed (collection-on-generator-stack + segfaulted; the suspended-stack scan was INVERTED — dead region + scanned, live frames missed) + LOS interior-pointer tolerance; + profile: self-compile = 79.5M allocs/3.4GB, 39% closureenv, + steady-state young survival 2.4-3.4% of bytes, pins 380-650 + objects/cycle (KBs — conservative pinning is a non-issue, so P2 + proceeds WITHOUT P3); runtime `-O2` landed: self-compile 127s→42s + (3.06×), types-bench2 2.00s→0.68s. +- [x] **gc-P1** 64-bit header (+ reserved shape/trace bits) + `lib/types.ts` + lockstep; forwarding helpers. + *Gate:* matrix green, all three bootstrap targets. + DONE 2026-07-24. The header half landed 2026-07-23 as the joint + shapes-P4.1 atomic change (u64 header, shape bits 32-55, mode bit + 56, lib/types.ts as two i32 halves); this phase added the + remainder: bit 59 = FORWARDED + first-word-overwrite forwarding + record (target address in bits 0-46 — the sub-2^47 NaN-box rule + makes the discriminator unambiguous), read/write helpers in + ejs-gc.h (`_ejs_gc_is_forwarded` / `_ejs_gc_forwarding_addr` / + `_ejs_gc_forward`), inert until gc-P2 and exercised by + EJS_GC_SELFTEST=1 at init; ejs-types.h now documents the complete + bit inventory (57 YOUNG / 58 PINNED from P0 profiling, 60-63 + still free for mark/card). Local matrix ×7 green; linux targets + ride the standing CI bootstrap matrix on push. +- [x] **gc-P2** nursery + inline `make_env` allocation + write barrier + + evacuating minor GC w/ cell pinning; old collector behind a flag + (`EJS_GC_NURSERY=off`), differential + stress lanes; heap-context + struct + context-accessor seam from the first line of new code. + *Gate:* alloc throughput ↑; minor p99 < 1 ms; differential green; pin + report; zero new file-static collector state. + DONE 2026-07-25 — docs/gc-p2-results.md has the numbers. Headlines: + object-remembering barrier (slot-address remset abandoned — dangling + recorded slots in freed malloc storage); nursery ON by default; + bench2 0.69→0.64s, envbench 1.82→1.48s, self-compile parity at 39s; + minor p99 0.68ms @512KB budget (1MB default = 1.27ms); the + conservative-lookup bounds prefilter that fixed two lookup + pathologies also sped the OLD collector's full marks (43.4→39.3s + self-compile). The war story: unrooted ejsval C statics in + ejs-llvm bindings — under a mover, roots exist to REWRITE + locations, not just keep referents alive. Deviation from the plan + line: no card table and no initializing-store elision — the + object-remembering DIRTY bit dedups repeat stores and modules/LOS + are handled by unconditional scan / born-dirty instead. +- [x] **gc-P3** gc-frame precise JS roots (chained variant) + env slot-address + inlining; move-everything stress mode. + *Gate:* stress green; pin-rate delta + spill-cost numbers recorded. + DONE 2026-07-25 — docs/gc-p3-results.md has the numbers. Headlines: + slot DEMOTION (store at def, load per use) rather than + spill/reload — dominance-safe by construction, forwarding-safe + because the frame escapes through the chain; per-stack chains + swapped by the generator hooks; pin-first ordering (a C-visible + object must not move). The bug measurement caught: the + conservative scan pinned every frame-held value through its own + stack-resident slot — minors now skip the scanned stack's frame + records. 76k relocations/self-compile, pins p50 373→101, net + wall cost ~+1% (env slot inlining pays back half the frame cost). + Deferred: invoke-form safepoints stay pinned; stackmap variant + unmeasured. +- [x] **gc-P4** mostly-copying major compaction + auto-tuned growth target. + *Gate:* heap shrink demonstrated; knob census = 1. + DONE 2026-07-26 — docs/gc-p4-results.md has the numbers. The + pin-scan cliff died first (the "first order of business"): one + PROT_NONE arena reservation at init + direct-map arena lookup + + LOS sorted-range bsearch; the A/B was brutal (baseline binary + stuck in the slow mode: >13 MINUTES for the self-compile the + fixed binary does in ~60s, sampled ~95% inside + mark_ejsvals_in_range→find_page_and_cell; fixed binary: 4 runs + within 62-64s, minor pause max 10.9ms, GC ≈ 10% of wall). + Compaction: conservative hits + registered generators set PINNED; + post-sweep sparse-first evacuation with the source set chosen + COMPLETELY before any evacuation (one-pass selection let an early + destination later become a source via its stale live count); + fixup = roots/modules/gc-frames/remset + all live cells. Shrink + gate: frag bench 37.6→8.3MB (4.5×), idempotent second collect; + self-compile full GCs free ~5k pages each. Growth target: + full_gc_trigger() = EJS_GC_GROWTH% (default 50) of post-sweep + footprint, 2-arena floor; knob census = 1. EJS_GC_COMPACT=off + for A/B. Drive-bys: LOS tail-page leak on free; + young-survivor-page full-sweep list corruption (young_page_freed). +- [x] **gc-P5** shapes intersection (sequenced by maam P4): trace bitmaps, inline + slots, object-literal inline allocation, typed-slot elisions. + DONE 2026-07-28 — docs/gc-p5-results.md has the numbers. + Headlines: single-cell shaped objects (embedded slots behind the + unchanged obj->slots ejsval; pointer-identity mode test; ctor + birth-capacity hint on EJSFunction), barrier owner flipped to + the wrapper object with Scan walking slot values, per-shape + f64 trace masks, born-shaped literals extended to flag-off, and + the 256-byte size class enabled (LOS allocs −69% on a compile + workload). litbench 1.84×, bench2 cells halved; emitted bump + allocation for literals measured at ~6% of an alloc-heavy loop + and deferred on that evidence (the ctor sink + hint already + cover construction). Matrix ×7, stress envs, and the --types + diff lane (475 files, 0 divergent) green. +- [ ] **gc-P6** collector thread: concurrent mark (SATB) + STW survivor + evacuation. + *Gate:* STW independent of live-set size. +- [ ] **gc-P7** (optional) Brooks + load barrier for concurrent evacuation — + only on Phase 6 evidence. diff --git a/docs/language-plan.md b/docs/language-plan.md new file mode 100644 index 00000000..999caaf4 --- /dev/null +++ b/docs/language-plan.md @@ -0,0 +1,57 @@ +# language-plan: JS modernization and conformance + +Bucket plan; the ordering spine lives in `docs/plans.md` (milestone +references look like `language-P1`). Content moved here from the old +plans.md "JS Modernization" section. + +JavaScript hasn't stood still while this project was on hiatus: there +are new language features to catch up on (optional chaining, nullish +coalescing, class fields, async/await, BigInt, ...), and the kangax +conformance suite this repo tests against has been superseded — tc39 +maintains test262, which is far larger. + +Sequenced after the TypeScript port (compiler-P2) — new-feature work is +safer with types underneath it. + +## Phases + +- [ ] **language-P1 — Gap inventory.** An initial 34-probe census + lives in `test/modernization/` (see its README). Headline: 13 + parser gaps (optional chaining, `??`, class fields, async/await, + `**`, object spread/rest, BigInt, ...), 4 stdlib gaps + (padStart/flat/Object.entries/globalThis), 4 behavioral bugs + (`__proto__:` literal, `/gi` replace, `generator.return()`, and a + hazard: `async m()` object methods parse but silently + miscompile). Remaining work: a test262 subset probe for + exhaustiveness, and a prioritized feature list from it. +- [ ] **language-P2 — Parser replacement.** Keep the slot + interface-shaped (the compiler consumes ESTree; parser behind one + module) with **@babel/parser + its estree plugin as the default** + — it's where stage proposals land first (decorators, pipeline, + pattern matching as enableable plugins); it's zero-dependency and + bundles flat for vendoring. Acorn remains the cheap-swap + alternative. The MAAM analysis framework consumes ESTree and has + no dependency on any particular parser — the compiler/analysis + contract is the ESTree shape of the post-desugar tree, so the + parser choice is free on both sides. Self-hosting wrinkle: + either parser's own source is newer JS than echojs parses, so + vendor a mechanically-regenerable transpiled build (babel to the + supported subset), shrinking the transpile step as modernization + features land. +- [ ] **language-P3 — Feature implementation, payoff-ordered.** Wire + probes into CI as they green. Syntax-only features (optional + chaining, `??`, `**`, spread/rest in objects) are desugar + candidates; async/await and class fields need runtime + emitter + work; BigInt needs a value-representation decision (NaN-boxing + has no spare tag appetite — likely heap-boxed). +- [ ] **language-P4 — test262 lane.** Stand up a curated test262 + subset as a CI lane (the kangax harness stays until parity); + grow toward the full suite as features land. +- [ ] **language-P5 — Un-fork the JS external-deps.** + esprima/escodegen/estraverse/esutils live in `external-deps/` as + lightly-patched copies (build-system compatibility). Move to + published npm packages where possible — published esprima is + unmaintained and still lacks the parser-gap features above, which + is what language-P2 solves; escodegen/estraverse/esutils can come + from npm as-is if the local patches prove to be build-glue only + (diff them first). diff --git a/docs/maam-p0-results.md b/docs/maam-p0-results.md new file mode 100644 index 00000000..c5e73b3a --- /dev/null +++ b/docs/maam-p0-results.md @@ -0,0 +1,1004 @@ +# MAAM Phase 0 measurement results + +Date: 2026-07-19. Chunk C of the Phase 0 plan (docs/maam-plan.md): run the +`--types` probe over real corpora and turn "does it converge on our sources, +at what cost" into numbers. Measurement only — no code changes. + +## Environment + +- echojs `eir` @ d3e3fd1 (probe as committed), submodule `echojs-maam` + `ejs-integration` @ 1bea5de, maam CJS dist built from that commit. +- node v22.4.0, macOS arm64 (Darwin 24.6.0), 32 GB RAM, llvm at + `/opt/homebrew/opt/llvm`. +- Compiler under test: node-hosted stage0 (`//lib:generated` babel tree), + run from a stage0-style work tree (`//:srcdir-tree` copy + `lib/generated` + + repo `test/`), the same layout `buck-test-stage.sh` assembles. +- Analysis spec (hardcoded in the probe): + `kCFA(1, "flow-sensitive", "call-site", shapeCap=64, false, false, false, stateCap=512)`. +- Raw logs + orchestrator/aggregator scripts: `~/.cache/maam-p0-logs/` + (`A-on/`, `A-off/`, `B/`, `*.summary.json`). + +## Command shapes + +Corpus A, per file (cwd = worktree`/test`, 120 s kill-timeout, concurrency 4): + + node /lib/generated/ejs-es6.js --srcdir \ + --moduledir ../node-compat --moduledir ../ejs-llvm --types .js + +Corpus B, one self-compile exactly like stage1 (cwd = worktree root): + + /usr/bin/time -l node lib/generated/ejs-es6.js --srcdir \ + --moduledir node-compat --moduledir ejs-llvm --types ejs-es6.js + +`PATH` prepends the llvm bindir; `NODE_PATH` = repo `node_modules` + +`node-llvm/build/Release`; `SDKROOT` from xcrun — mirroring +`buck-test-stage.sh`. The work tree must live **inside the repo checkout** +so the probe's upward walk can locate `external-deps/echojs-maam` (see the +dev-tree-only note in maam-plan.md); from `/tmp` the probe warns +"could not locate the submodule" and analyzes nothing. + +## Corpus A — `test/*.js` (457 files, all run; no sampling needed) + +Full run at concurrency 4; the 120 s per-file budget was never approached. + +| class | files | notes | +|---|---|---| +| analyzed, degraded-with-warning | 362 | ≥1 module with warnings (see kinds below) | +| analysis-failed (warn-wrapped) | 84 | all `NormalizeError`; compile continues, exit 0 | +| analyzed, clean (`warnings=none`) | 10 | closure-test1, closure4, closure7, eir-interop1-lib, eir-ns1-lib, eir-syntax2-lib, object1, reexport1-lib, reexport2-mid, void0 | +| TIMEOUT (>120 s) | 0 | | +| compile-N/A | 1 | `tester.js` — parse error in the esprima fork, fails identically without `--types` (exit 255 both ways) | + +**Zero compile failures caused by `--types`.** Every failure mode above is a +warning; the one non-zero exit fails flag-off too. Parity spot-check: 21 +files (every 23rd, alphabetically, plus tester.js) compiled flag-off — 21/21 +exit codes identical to the flag-on run. + +Warning kinds across the 413 analyzed modules (module counts): +`unknown-call` 378, `polymorphic-function` 27. `unknown-call` is ubiquitous +because nearly every test calls `console.log` (counted as an unknown method +call since the Chunk A metric broadening). + +`NormalizeError` breakdown (84 files): + +| count | message | +|---|---| +| 26 | only plain identifier parameters are supported (no destructuring/defaults/rest) | +| 22 | unsupported statement: ForOfStatement | +| 8 | unsupported statement: LabeledStatement | +| 7 | unsupported expression: TemplateLiteral | +| 5 | computed object keys | +| 4 | Object.defineProperty requires a string-literal key | +| 3 | TaggedTemplateExpression | +| 3 | object getters/setters | +| 2 | unsupported expression: VariableDeclaration | +| 2 | non-identifier object keys | +| 1 | DebuggerStatement | +| 1 | defineProperties non-literal descriptors | + +Wall time (analysis only, per module; n=413): median 5 ms, p90 12 ms, max +5032 ms, total 20.9 s. Whole-compile wall per file (includes llc + clang): +median 333 ms, p90 367 ms, max 7.3 s. Flag-on vs flag-off median on the +21-file sample: 335 ms vs 323 ms (~+4%). + +Slowest analyses: esprima-es6 (5.0 s; pulled in by esprima1/ +esprima-roundtrip1/2 as an import) — 3 appearances; typedarray2.js (2.2 s, +832 states, 32 994 iterations); fib.js (0.4 s, 9 687 iterations). + +unknownCalls per module: median 3, p90 15, max 168 (typedarray2), total +2 694. Top: typedarray2 168, math1 132, error1 70, typedarray14 69, +typedarray15 63. degradedBindings: 0 everywhere — post-desugar test trees +carrying `rest` never reach analysis (they bounce off the destructuring- +parameter NormalizeError first). + +## Corpus B — the compiler's own generated JS (stage1 self-compile input) + +One `--types` self-compile of `ejs-es6.js`: **45 modules, exit 0, executable +produced and linked.** Real time 11.35 s vs 6.15 s flag-off (+5.2 s, +85%); +4.74 s of the delta is the single esprima-es6 module. Max RSS 287 MB +(`/usr/bin/time -l`; 10 s RSS polling agrees) — nowhere near the 4 GB watch +threshold. No timeouts; per-module analysis never exceeded 4.8 s. + +| class | modules | +|---|---| +| analyzed (stats emitted) | 15 | +| analysis-failed (warn-wrapped NormalizeError) | 30 | +| TIMEOUT | 0 | + +Failure breakdown: TemplateLiteral 15, ForOfStatement 13, +destructuring/defaults/rest parameters 2. The compiler's own `lib/` modules +are written in modern JS; their post-desugar form still contains template +literals and for-of (EIR lowers those natively; nothing desugars them away +before the probe), so maam's normalizer rejects 2/3 of the compiler by +module count — including every big module (`compiler`, `eir/lower`, +`eir/optimize`, …). + +The 15 analyzed modules (analysis wall): esprima-es6 4 740 ms (102 states, +157 iters, 2 unknownCalls, polymorphic-function+unknown-call warnings), +escodegen-es6 62 ms, estraverse-es6 35 ms, esutils/lib/code 34 ms (739 +iters), sret-abi 6 ms, abi 3 ms, common-ids 3 ms (69 unknownCalls), +host-config 3 ms, plus 7 more ≤3 ms, all `warnings=none` or a single +unknown-call. degradedBindings: 0 on all 15. + +Determinism: a second full `--types` self-compile produced byte-identical +stats lines (wall stripped) — reachedStates/iterations/unknownCalls all +stable across runs. + +## Cap behavior + +Not observable. Neither `describe()` nor `metrics` exposes stateCap or +shapeCap hit counts; nothing in the output distinguishes "converged +naturally" from "converged because the cap smeared contexts". (esprima-es6's +5 s / 102-state / 157-iteration profile is wall-heavy but state-light, which +suggests time goes to store joins on very large flow-sensitive stores rather +than state explosion — but that is inference, not measurement.) **Finding +for Phase 1: surface cap-hit counters (`funcContexts` saturation, shape +widenings) in `metrics`.** + +## Reading (factual) + +- **Convergence verdict, Corpus A:** converges everywhere it runs; zero + timeouts; analysis is noise next to codegen (median 5 ms vs 333 ms + compile). Decision-rule outcome **(a)** for the gate corpus. +- **Convergence verdict, Corpus B:** converges on everything it can parse, + at +5.2 s on an 11 s self-compile — tolerable for an opt-in flag. But the + measured set excludes every compiler-sized module: the largest thing + actually analyzed was esprima-es6. The plan's headline question ("do + compiler-sized modules converge?") is **still open — blocked on dialect + coverage, not on the engine.** The binding constraint Phase 0 found is + maam's normalizer coverage, not convergence or cost: TemplateLiteral, + ForOfStatement, and destructuring params account for 100% of Corpus B's + rejects (30/30), ~65% of Corpus A's (55/84), ~75% combined (85/114; ~77% + if TaggedTemplateExpression is counted into the template family). +- **Where time goes:** esprima-es6 dominates both corpora — its three + appearances in Corpus A (imported by the esprima tests) are 71.7% of A's + total analysis time (24.1% for a single appearance), and in Corpus B it is + 96.9% of analysis time and 91% of the flag-on/off wall delta; everything + else is ≤62 ms. Whatever + makes esprima slow (large flow-sensitive stores is the hypothesis) is the + first profiling target if bigger modules join the corpus. +- **Implication for the decision rule:** no evidence for (b) or (c); no + widening pressure observed and nothing failed to converge. But (a) can + only be provisionally claimed: the modules that would stress the engine + never reached it. The cheapest way to make Phase 0's question answerable + is normalizer coverage for the three dominant constructs, then re-run this + measurement — that decision belongs to Phase 1 planning, not this doc. +- `degradedBindings` never fired on either corpus; the rest-parameter + degradation path is currently exercised only by maam's own unit tests. +- `unknownCalls` is dominated by stdlib/console usage; per the plan's Future + work note, these numbers are not comparable to the maam repo's pre- + broadening paper tables. + +--- + +# Phase 1 re-measurement (Chunk E) + +Date: 2026-07-19 (runs) / 2026-07-21 (aggregation). Same protocol, +environment, and command shapes as the Phase 0 measurement above; the only +change is the analyzer: submodule @ 1e09ffd (⊤-degradation; TemplateLiteral / +ForOfStatement / pattern coverage; cap-hit counters; S1 closure-fingerprint +iteration degrade). echojs @ 3e71ad4 (no compiler changes since d3e3fd1). +Raw logs: `~/.cache/maam-p0-logs/` (`B2/`, `A2-on/`, alongside the Phase 0 +`B/`, `A-on/` for diffing). + +## Corpus B — compiler self-compile (the headline) + +**38 of 45 modules analyzed (was 15), exit 0, executable linked, no +timeouts.** Every compiler-sized module now reaches the engine and +converges: + +| module | wall | states | iterations | caps | +|---|---|---|---|---| +| ejs-es6.js (driver) | 4007 ms | 1050 | 29 615 | none | +| esprima-es6 | 4609 ms | 106 | 210 | none | +| lib/passes/desugar-classes | 120 ms | 68 | 68 | shapeCap 1 | +| escodegen-es6 | 68 ms | 166 | 231 | shapeCap 1 | +| lib/eir/ops | 66 ms | 212 | 212 | none | +| lib/eir/optimize | 39 ms | 14 | 14 | none | +| lib/eir/lower | 29 ms | 115 | 115 | shapeCap 1 | +| lib/compiler | 10 ms | 74 | 74 | shapeCap 1 | + +Aggregates (n=38 modules): analysis wall median 3 ms / p90 68 ms / max +4609 ms / **total 9.1 s**. Real time 15.28 s flag-on vs 5.73 s flag-off +(+9.6 s; esprima-es6 + the ejs-es6 driver account for 8.6 s of it). Max +RSS 575 MB (was 287 MB) — well under the 6 GB watch line (raised from +Phase 0's 4 GB for this run). Totals: +unknownCalls 202, degradedBindings 136. + +**Cap behavior (now observable):** stateCap (512): **0 hits anywhere** — +including the 29 615-iteration driver analysis. shapeCap (64): exactly +**1 hit in each of 10 modules** (triple, abi, compiler, eir/emit, +eir/builder, eir/lower, eir/scopes, escodegen, estraverse, desugar-classes) +— one megamorphic collapse per module, consistent with a single object +built up field-by-field under weak updates. Convergence is natural, not +cap-forced, everywhere it matters. + +**Remaining rejects (7, was 30):** 6 × "only plain identifier or +destructuring-pattern parameters" + 1 × `Object.defineProperty` non-literal +key (lib/runtime). Root cause of the 6, identified by inspection: echojs's +`DesugarDestructuring` keeps a trailing **`RestElement` in `params`** ("a +trailing ...rest stays in place — EIR handles it natively", +lib/passes/desugar-destructuring.ts:244) — maam's `compileFunction` accepts +the old-esprima `.rest` *field* but not a RestElement param. A one-line +coverage item (treat a trailing RestElement param exactly like the dialect +`rest` field); affected: ast-builder, node-visitor, consts, +desugar-metaproperties, desugar-spread, desugar-destructuring. + +## Corpus A — test/*.js (457 files, full rerun) + +| class | Phase 0 | now | Δ | +|---|---|---|---| +| analyzed, degraded-with-warning | 362 | 391 | +29 | +| analysis-failed (warn-wrapped) | 84 | 55 | −29 | +| analyzed, clean | 10 | 10 | — | +| TIMEOUT | 0 | 0 | — | +| compile-N/A (tester.js, flag-independent) | 1 | 1 | — | + +Zero `--types`-caused compile failures again (only tester.js exits +non-zero, identically flag-off). Remaining reject histogram: param-kind 28 +(the same RestElement gap as Corpus B), LabeledStatement 8, defineProperty +non-literal key 5, computed object keys 5, getters/setters 3, misc 6. +TemplateLiteral, ForOfStatement, TaggedTemplate, and +destructuring-declaration rejects are **gone** (22 + 7 + 3 + the pattern +share of the old 26-count bucket in the Phase 0 histogram). + +Wall time is unchanged: analysis per module (n=442) median 6 ms / p90 +13 ms / max 5105 ms (esprima again) / total 21.0 s; per-file compile wall +median 334 ms (was 333), p90 362, max 7.2 s. Cap hits: shapeCap 1 in 6 +modules across the 3 esprima-importing files; stateCap 0 everywhere. + +## unknownCalls / degradedBindings deltas (S1 caveat quantified) + +- Corpus A total unknownCalls 2694 → 2935 (+241). Decomposition: **+218 + from the 29 newly-analyzed files** (code the engine never saw before — + console/stdlib externals plus iterator-protocol degradations; not + separable per-kind in current metrics); **+36 across 9 of the 372 + previously-analyzed files — all increases** (set3 +17, array30 +7, + typedarray10 +4, … — none containing for-of; this is ⊤-propagation + reaching branches a false `undefined` used to kill). The **−13 is a + double-count correction**, not a decrease on any file: eir-promo1.js was + analysis-failed in Phase 0 but had emitted partial stats (13 unknownCalls + already inside the 2694 total), and its full re-count now sits inside the + +218 bucket. 2694 + 218 + 36 − 13 = 2935. +- Corpus B: the 15 previously-analyzed modules are **stable — zero changed + unknownCalls**; the +128 rides on the 23 newly-analyzed modules (driver + 98, lib/types 26, everything else ≤2). +- **S1 (closure-fingerprint iteration degrade): no measurable inflation on + previously-analyzed code in either corpus.** The feared + for-of-over-function-arrays cost did not surface at corpus scale; if + per-kind attribution is ever needed, a degradation-kind counter is the + follow-up. +- degradedBindings: 0 → 66 (Corpus A) / 136 (Corpus B) — now counting + unmodeled imports and rest parameters as designed; an imports-only module + no longer masquerades as a closed world. + +## Reading against the decision rule + +- **The Phase 0 open question is closed: outcome (a) — ship as-is behind + `--types`.** Compiler-sized modules reach the engine and converge + naturally: 0 stateCap hits corpus-wide, shapeCap touched exactly once in + each of 10 of the 38 modules, the largest analysis (29 615 iterations) + finishes in 4 s, total + self-compile overhead +9.6 s on an opt-in flag, RSS 575 MB. No evidence + for (b) heavy-widening or (c) non-convergence anywhere in either corpus. +- Where time goes is unchanged in kind: esprima-es6 (wall-heavy, + state-light — store-join cost hypothesis stands) plus, now, the ejs-es6 + driver (iteration-heavy, converges clean). Everything else ≤120 ms. +- The binding constraint has shrunk from "three constructs blocking every + big module" to **one one-line gap (RestElement params) plus a small + tail** (labels, non-literal defineProperty keys, computed keys, + accessors-in-literals) — with only the RestElement gap blocking any + module of consequence. + +--- + +# Phase 3 gates (Chunk J) + +Date: 2026-07-22. echojs @ 568efc7 (oracle-guided guarded arithmetic in +lowering), maam @ 8d6a157. Same environment as the earlier measurements +(node v22.4.0, macOS arm64, llvm @ /opt/homebrew/opt/llvm); everything runs +color-free (`NO_COLOR=1`, `FORCE_COLOR` unset — a colored-env buck daemon +poisons regenerated expected files; lesson institutionalized in the lane +script). Raw logs: `~/.cache/maam-p0-logs/J*` (diff-lane per-file logs + +results.jsonl; the microbenchmark timings below are recorded here only — +the timing runs left no separate artifact). + +## The --types diff lane (the behavioral gate) + +`./buck-test-types-diff.sh [conc]` — every test/*.js +compiled flag-off AND with `--types`, both executables run, RUN STDOUT +byte-compared (stderr excluded by design: `--types` stats lines, and the +debug runtime's `EXCEPTIONS:` traces on normally-handled exceptions). +Per-file 120 s timeouts, concurrency 4, per-worker TMPDIRs (concurrent +compiles never share temp space). + +| files | identical | divergent | N/A | timeouts | +|---|---|---|---|---| +| 458 | 457 | **0** | 1 (tester.js, esprima parse gap — fails flag-off too) | 0 | + +Aggregates from the `--types` stats lines: **diamonds 67**, oracleQueries +1319, oracleUnknown 1035. The high unknown share is expected on this +corpus: operands inside functions the per-module analysis never reaches +(exported-only / callback-only bodies, dead branches) query as unknown → +top → no diamond — the guard-shaped degradation working as designed. The +suite is string/object-heavy; 67 diamonds concentrate in the numeric +files. + +## test/types/ probes + +Seven standalone probes (see test/types/README.md for the per-file table): +diamond-eligible shapes fire (locals 6, params 4, literals 5, loops 6, +bench kernel 9); reassignment-widened bindings do NOT diamond (0, by +design — only exact {number} qualifies); and the wrong-oracle case — a +cross-module call handing a string to a parameter the callee's module +analysis typed {number} — routes through the has_tag guard to the slow +path and prints the correct "x1" with flag-off/--types outputs identical. + +## Microbenchmark + +test/types/types-bench1.js: 40 × 1 M-iteration kernel of +`s = s + i*i - i/2; i = i + 1` under a `<` loop guard — all module-local, +everything oracle-typed {number}; diamonds=9, oracleUnknown=0. Compiled +flag-off vs `--types`, run 7× each interleaved (`/usr/bin/time -p`, same +machine, no other load; distributions were tight — no GC-outlier rerun +needed): + +| build | median | min | max | +|---|---|---|---| +| flag-off | 3.19 s | 3.18 s | 3.20 s | +| --types | 0.31 s | 0.31 s | 0.32 s | + +**10.3× median speedup**, identical program output (13333303333341514000). +Honest caveats: this kernel is the best case — per-iteration generic +runtime binop calls dominate the flag-off build, and the typed build +replaces essentially all of them (9 diamonds cover the kernel's every +operator). Real modules keep their surrounding generic ops; the suite-wide +effect is bounded by the 67-diamond density above, and unbox/box round +trips still go through memory (the bits_alloca idiom), so further headroom +remains for a Phase 4-era register-level cleanup. + +## Matrix + stage2 ≡ stage3 (flag off) + +Full serial matrix green: //:test-eir (incl. the typed-arith EIR-shape +tests), //:test-eir-lowtier (the injected low-tier e2e), //:test-stage0..3. +Functional stage2 ≡ stage3 gate, per the reading this document establishes +(raw binary byte-identity does not hold on macOS for linker-metadata +reasons): stage2 and stage3 each compile and run the ENTIRE test corpus in +identical buck-assembled work dirs with per-test expected-output +comparison — both green constitutes the corpus-level functional-identity +check. Flag-off byte-purity of the Phase 3 lowering itself was +additionally proven at Chunk I review time (pre- vs post-chunk flag-off +executables byte-identical). + +## Reading + +Every P3 gate item holds: zero behavioral divergence across the suite with +the flag on; the probe dir documents exactly which shapes fire and which +degrade (widening, wrong oracle — both by design); the mechanism-level +proof (//:test-eir-lowtier) is now backed by a magnitude measurement (10× +on a pure-numeric kernel, a ceiling not a promise); matrix unaffected flag +off. Phase 3 is complete pending sign-off. + +# Phase 3.4 gates (diamond pre-work) + +Date: 2026-07-22. echojs @ eir (this commit; passes in +lib/eir/optimize-guards.ts), maam @ 8d6a157. Same environment and +color-free protocol as the Phase 3 measurements. + +## The passes (what changed) + +Two trust-free optimizer passes over the Phase 3 diamonds — nothing here +consumes an oracle claim; every fact is proven from the IR, so a wrong +oracle still only costs speed: + +- **(a) dominated-guard elimination + guard-region merging.** Dominance + reasoning: the CHK dominator tree (shared with the verifier) plus the + sole-predecessor-TRUE-edge condition — entering such a successor is + equivalent to its guard having held, and SSA number-ness is immutable — + combined with value-intrinsic proofs (const number, box_f64, and + generic mul/div/sub results, which are always numbers in both ES and + runtime/ejs-ops.c). Region merging structurally VERIFIES (never + assumes) the diamond shape — effect-free fast side, whitelisted + {add,sub,mul,div,lt} slow chain — then fuses adjacent regions into one + guard region with ONE slow path (the full generic computation in + program order). Guard failures after partial fast execution re-enter + the slow chain from the top; the merge first proves that re-execution + is pure and value-identical (operands guard-proven numbers), else it + refuses. +- **(b) raw f64 block params for optimizer-rewired joins.** A param + whose every incoming argument is a strippable box_f64 / f64 value / + converted param becomes an f64 phi (double in the emitter), killing + the bits_alloca box/unbox round-trips between merged diamonds; any + remaining boxed use re-boxes exactly once at the region exit. The + verifier's P2 rule is lifted ONLY for params carrying the new + `rawJoin` marker, and the marker is provenance rather than trust: the + verifier independently re-checks type-f64, all-args-f64, non-catch, + no-unwind-edge — an f64 param WITHOUT the marker is rejected, so every + lowering-created edge keeps the strict boxed rule. + +hypot2 acceptance shape (see the regenerated +`~/src/echojs/hypot2-types-before-after.txt`): three diamonds / six +has_tags as lowered → ONE region with one has_tag per distinct value +(2), one slow chain (mul/mul/add), fast side unboxed end-to-end through +`phi double` joins, one box_f64 at the region exit. + +## The --types diff lane (behavioral gate) + +Clean re-assembled work tree, identical protocol: + +| files | identical | divergent | N/A | timeouts | +|---|---|---|---|---| +| 458 | 457 | **0** | 1 (tester.js, unchanged) | 0 | + +Aggregates: **diamonds 67**, oracleQueries 1319, oracleUnknown 1035 — +byte-for-byte the Phase 3 numbers. The lane counts LOWERING's diamonds +and the passes run post-hoc, so the count is unchanged by design; the +lane's expectations needed no touch. Both vacuous-pass guards +re-verified to trip: an empty work tree exits 1 ("zero files compared"), +and an outside-the-repo tree (dead oracle) exits 1 ("diamonds total is +0"). An additional superset run (467 files: the 458 plus probe/demo +copies) was also 0-divergent. + +## EIR-shape unit tests + +//:test-eir green, 114 tests, including the new Phase 3.4 shapes: +merged hypot2 (exactly 2 has_tags, a single guard-failure target, the +generic mul/mul/add surviving on the one slow path, box_f64 exactly +once, f64 rawJoin params on the intermediate joins); the bench-kernel +statement chain merging across pure const prefixes (six diamonds → 2 +has_tags, 1 slow path, 1 box); a negative shape (guards in an if-branch +do not dominate a later re-test: nothing folds, nothing merges, no raw +params); and the verifier triple (rawJoin accepted; f64 param without +the marker rejected; boxed arg into a rawJoin param rejected). + +## Microbenchmark (types-bench1, deltas vs Phase 3) + +Same kernel, same protocol (7× interleaved, /usr/bin/time -p): + +| build | P3 median | P3.4 median | note | +|---|---|---|---| +| flag-off | 3.19 s | 3.21 s | unchanged (five runs 3.17–3.37; two hit background-load noise at 4.96/5.70 — kept in, the median absorbs them) | +| --types | 0.31 s | **0.23 s** | −26% typed runtime | + +**Speedup 14.0× median (was 10.3×)**; diamonds=9, oracleUnknown=0, +output identical (13333303333341514000). Remaining headroom is the +region BOUNDARIES: loop-carried params and call arguments still box +(entry args are consts/params, not box_f64 — deliberately outside pass +(b)'s proof), which is P3.6's typed-calling-convention territory. + +## hypot2 demo (deltas vs Phase 3) + +diamonds=7 (unchanged — lowering's count). Wall time unchanged within +noise (flag-off 2.71/2.51/2.60 s, --types 0.51/0.34/0.35 s, ~7×): the +demo is dominated by the boxed call/closure/loop overhead around +hypot2, which P3.4 does not touch. What changed is the emitted shape — +2 NaN-box checks instead of 6, `phi double` fast pipeline, one generic +chain, one box — recorded with before/after EIR and LLVM excerpts in +the regenerated dump file. + +## test/types probes + +All seven probes still match (`node` diff / flag-off≡--types for the +wrong-oracle case), per-file diamond counts identical to the census +(6/4/5/0/6/9; wrongoracle lib=1). The wrong-oracle keystone still +routes the cross-module string through the guard to the slow path and +prints "x1" with identical flag-off/--types output. + +## Matrix + stage2 ≡ stage3 (flag off) + +Full serial matrix re-run on the final code: //:test-eir, +//:test-eir-lowtier, //:test-stage0..3 — six of six BUILD SUCCEEDED +(grep-verified in the buck logs, never tail exit). stage2 ≡ stage3 +functional gate (both stages compile and run the entire corpus with +per-test expected-output comparison) green. Flag-off the new passes +bail before touching anything: optimizeGuardRegions scans for number +guards and returns (none exist without --types), so flag-off output is +untouched by construction and the stage gates confirm it. + +## Reading + +Both P3.4 items hold with zero behavioral divergence: dominated guards +fold and adjacent diamonds merge into single-slow-path regions on real +dominance reasoning; the raw-f64-join lift is scoped by a +verifier-re-checked marker rather than a global weakening; the +microbenchmark ceiling moves 10.3× → 14.0×, and the remaining box/unbox +traffic sits exactly where P3.6 (typed calling convention) picks up. + +## Review fixes (adversarial pass over the merge) + +Two latent unsoundnesses were found by adversarial review on +verifier-valid IR (neither constructible from JS through today's +lowering, both violations of the "structurally verified, never assumed" +contract) and are fixed with localized pre-checks in `tryMergeAt`, each +with a refusal unit test: + +- **J1 predecessor exhaustiveness**: j1's predecessors must be exactly + region1's exits (mirroring the existing j2 check). A foreign edge + into j1 made region2's guards reachable without region1 having run, + while the merge substituted region2's slow operands with region1's + slow-side values — wrong on the foreign path. +- **Generic-twin verification** (`verifyGenericTwin`): region2's slow + chain must be the generic rendition of its fast side — same + arithmetic ops in the same order, operands corresponding under the + box/unbox mapping, join-exit args corresponding slot for slot. The + reroute sends executions whose region2 guards would have passed + (e.g. a guard on a mul result) through the slow chain; twin-ness is + what makes that value-identical. One deliberate narrowing: a region2 + containing `f64_lt` now declines to merge (the boolean-twin + correspondence buys nothing measurable; lt regions still merge as + region1) — hypot2/bench shapes and stats are unaffected + (regions_merged and all measured numbers unchanged; bench re-verified + at 0.23 s). + +Also from review: routing explicitly refuses raw-typed (i1/f64) +j1-values live past region2 (fail-closed, now documented + enforced up +front); the loop-carried rawJoin conversion (a fully-proven f64 loop +param) gained a dedicated unit test; ir.ts's rawJoin comment now states +the actual contract (structural qualification, verifier-checked — not +provenance-linked). + +# Phase 3.5 — differential harness (concreteEval vs node vs ejs) + +Date: 2026-07-23. echojs @ eir (P3.4 head), maam @ c69fc81, revised same +day to maam @ 3e64ca1 (review round 2) and maam @ c3d1aed (round-3 nits +R1/R2; the review subsections below record what changed — numbers in this +section are the FINAL c3d1aed figures). Deliverable +lives in the maam repo: `test/differential/harness.ts` + a 40-file +closed-world corpus, run by `npm run diff-harness` and wired into the new +maam CI workflow (`.github/workflows/ci.yml`, node pinned 22.4.0). The +concrete interpreter — `analyze(prog, concreteEval() + intrinsics)` — is the +reference semantics; the harness diffs it against node, against +ejs-compiled output, and against the abstract oracle configs. + +## Comparison semantics (the deliberate choices) + +The corpus convention is that a file's last top-level statement is an +ExpressionStatement; its value is the file's *final value* (for that shape +it coincides with the completion value). Comparison is **value-level**, +not host-stringification: the harness wraps that expression in an injected +ES5 renderer (−0 renders `"-0"`, NaN `"NaN"`, strings escaped by hand) and +the same renderer is applied to the concrete CVals, so number→string is +the identical algorithm on both sides. node's printed value must be a +MEMBER of the concrete result set; singletons must match exactly. +Documented blind spots: object/function finals compare by type only +(corpus files project structure into primitives), and non-singleton sets — +from the machine's two deliberate over-approximations, the smashed array +`elements` bucket and the always-reachable nondet catch handler — are +reported as PASS-CONTAINS, never silently. Analysis runs per file in a +worker subprocess under a 30 s budget: genuine concrete-machine divergence +(nondet for-of/for-in × unbounded concrete time) becomes a *visible* skip. + +## Gate results + +- Corpus 46 files. node lane: **37 exact, 3 membership, 0 divergences**; + 6 skips, all deliberate and printed with reasons (Math.random + nondeterminism; array prototype methods degrade under the concrete + domain; two files that prove the for-of/for-in divergence timeout path; + one each proving the nested-block-var and pattern-leaf-capture visible + degradations). The + differential lane exercises **zero iteration-protocol semantics** — + for-of/for-in are exactly the skip files, because their nondet iteration + never converges under unbounded concrete time. +- Containment lane: **1935 node checks, 0 violations** across two abstract + configs — the echojs oracle spec verbatim + (`kCFA(1, flow-sensitive, call-site, shapeCap=64, stateCap=512)`) and the + same + `intrinsics: true`. Checked-node set: every source node BOTH the + concrete and the abstract run map (the concrete entries are exactly what + a real execution produced). **Census of the exempt remainder** (57 + concrete-mapped nodes unmapped abstractly, summed over both configs; the + harness prints the count so growth is visible): these are NOT all dead + code — under config A (intrinsics off) they include LIVE coercion + arithmetic whose receiver/operand chain passes through an unbound global + (`Math.PI * 2 * 2`, `Number.MAX_VALUE * 2`) plus coercion forms the + normalizer maps but config A's ⊥-receiver paths kill (`true+1`, `""-1`, + `-"3"`, `+true` in the coercion files). Honest reading: the oracle + currently produces NO facts for such nodes (fail-soft ⊤ at the + consumer), so containment there is vacuous — they are exempt, not + verified. +- ejs lane (dev tree only; `MAAM_DIFF_EJS_TREE` = a stage0 work tree — + `//:srcdir-tree` copy + `lib/generated`; the lane skips loudly when + unset, e.g. in maam CI): **32 ok, 1 N/A (esprima `**` family), 7 + known-divergent, 0 new, 0 stale**. + +## What the harness found (the product) + +Fixed in maam (each with a pinned test; suite 241 → 258): + +1. **Closure-capture unsoundness** — a closure created textually at or + before a variable's declaration in the same statement list (a hoisted + function declaration — and, per review round 2, equally a function + expression, arrow, or object-literal method) referencing that variable + left the name un-renamed; closure writes silently missed the binding + (`var f = function () { n = "x"; }; var n = 0; f(); n` reported `num` + with zero degradation — a mapped-and-wrong oracle fact an unguarded + consumer would miscompile on). normStmts now detects capture with a + syntactic over-approximate scan over ALL function-creating subtrees, + positionally (capture at statement i ≤ declaration j), pre-binds + captured names to `undefined` above everything, and turns their + declarations into `setVar` writes. Declare-then-capture shapes keep + the precise fresh-binding path (no `undefined` widening), pinned by a + typeOfNode unit test. +2. **⊥-receiver property reads fabricated `undefined`** — with intrinsics + off, `Math.PI` read as a *confident* undefined (the containment lane + caught this as `num ⋢ undefined`). ⊥ receivers now propagate ⊥. +3. **String relational comparison was numeric** — `"a" < "b"` was false. +4. **ToNumber(null) was NaN in binops** — `1 + null` computed NaN, JS says 1. +5. **`s.length` read as confident undefined in both domains** — now exact + under the concrete domain, `anyNum` abstractly, ungated from the + intrinsics knob (the echojs oracle runs intrinsics-off). + Plus: `Infinity`/`NaN` identifiers were unbound (path-killing ⊥); they + are dialect literals now. + +Also built: exact concrete intrinsics — the plan's `intrinsics: true` +silently degraded under the concrete domain (seeded globals were ⊥). The +domain gained an optional `concretize` capability whose presence is the +exactness contract: pure-primitive intrinsics compute their real JS result +or the call degrades visibly through `unknownCalls`; summary models never +run concretely. + +Found in echojs, root-caused by minimal probes, recorded in +`ejs-known-divergences.json` (a listed file that *stops* diverging fails +the gate as stale, so the list can only shrink by fixing echojs): + +1. `typeof null` → `"null"` (spec: `"object"`). +2. `-0 === 0` → false (NaN-boxed bit comparison; `1/-0` is correct). +3. `Math.round(-2.5)` → −3 (C `round()` half-away-from-zero; JS: −2). +4. `Number(" 7 ")` → NaN (ToNumber(string) does not trim whitespace). +5. `-8 >>> 28` → 0 (ToUint32 on negative shift operands). +6. `1 + null` → runtime abort (`ejsval ToNumber(ejsval)`, + runtime/ejs-ops.c:260 "not implemented", exit 134). +7. esprima cannot parse `**` (arith-basic.js is the lane's one N/A). + +## Known model limits (documented, visible, tracked) + +- try/catch: handler modeled as always-reachable nondet with a ⊤ caught + value (sound over-approximation; membership-checked). `return` through + `finally` skips the finalizer in the model — corpus avoids the shape. +- `F.prototype = Object.create(...)` (prototype REASSIGNMENT) is + unmodeled and degrades visibly; the dialect shape is + `Object.setPrototypeOf`, which is modeled. +- for-of/for-in accumulation diverges under concrete time (nondet + iteration); the harness's worker timeout makes it a visible skip. +- Nested-block `var` hoisting is not modeled; when such a var is captured + by a function in the enclosing scope the normalizer now COUNTS it as a + degraded binding (review F2), so the harness precondition trips and the + file skips visibly instead of computing on ⊥. Destructuring-pattern + LEAVES captured at-or-before their declaration are likewise unmodeled + and, without the round-3 accounting, were SILENTLY WRONG (writes + dropped, zero counters) — they now count as degraded bindings too + (review R1). Re-declared (`var x` twice) captures ARE modeled: both + declarations assign the one pre-minted binding. +- Captured-by-closure vars now (correctly) include `undefined` in their + nodeTypes join from the hoisted pre-binding; non-captured and + declare-then-capture vars are unaffected. Oracle-fact impact measured + by the `--types` diff lane re-run below. + +## Review round 2 (adversarial pass over the harness commit) + +The review confirmed the harness mechanics (wrap seam, gate teeth under +perturbation, sigLeq, ejs-lane authenticity, CI viability) and rejected on +one confirmed HIGH finding plus process items; all addressed at maam +3e64ca1: + +- **F1 (the blocker): capture fix was FunctionDeclaration-only.** A + closure created textually at-or-before a later same-scope `var` via a + function expression / arrow / object-literal method still dropped its + writes silently — concrete `{num 0}` with zero degradation for + `var f = function () { n = "x"; }; var n = 0; f(); n;` while node says + "x", and the oracle reported a mapped-and-wrong `num`. Fixed by + replacing the compiled-freeVars detection with a syntactic + over-approximate scan over ALL function-creating subtrees (positional: + capture at statement i ≤ declaration j; declarations count as i = −1). + Three corpus probes (capture-fnexpr/arrow/objmethod.js) now PASS + exactly — the fix computes, it does not degrade. +- **F2: nested-block `var` capture now counts.** Previously concrete ⊥ + with zero accounting; the normalizer pushes a degradedBinding so the + harness skip precondition trips (skip-nested-var-capture.js proves it). +- **F3: unit pins independent of the harness** (review showed reverting + normalize.ts kept all 258 then-tests green): hoisted/expression/arrow/ + method capture, declare-then-capture precision (typeOfNode stays exact + `num`), nested-var visible degradation, bare NaN / Infinity literals, + and the ⊥-receiver read, each flipping if its fix is reverted. Suite + 258 → 266. +- **F4: known-divergence entries participate in staleness even when + unvalidatable** — a listed file that goes compile-N/A, is skipped, or + leaves the corpus is warned about by name (warning, not hard failure: + N/A means the run-behavior claim cannot be tested in either direction, + and hard-failing would let an esprima parse gap flip a semantics gate). + Entries are now structured ({symptom, rootCause}, enforced). +- **F5: the containment-exempt census is documented above** (the + 57-node remainder includes live coercion arithmetic under config A — + exempt, not verified — with the count printed every run). +- **F6: compound assignments corpus file added** (esprima-clean, so it + has full three-lane coverage; the `**` family lives in the expected-N/A + arith-basic.js); the zero-iteration-protocol statement is in the gate + results above. + +Final harness numbers at 3e64ca1 (all lanes): corpus 45 — node 37 exact + +3 membership + 5 visible skips, 0 divergences; containment 1935 checks, 0 +violations; ejs 32 ok / 1 N/A / 7 known / 0 new / 0 stale. + +## Review round 3 (nits R1/R2, maam c3d1aed) + +- **R1**: destructuring-pattern leaves captured at-or-before their + declaration were still silently wrong with zero counters + (`var f = function () { a = 9; }; var [a, b] = [1, 2]; f(); a;` → + concrete 1, real JS 9). Same remedy as F2: the normalizer records a + degradedBinding (harness precondition trips; skip-pattern-leaf-capture.js + proves the visible skip; declare-then-capture leaves pinned as + non-degrading; identifier-declared names excluded — the modeled path + owns them). Suite 266 → 268; corpus 45 → 46. +- **R2**: the capture scan early-returned after params + body, missing + closures inside old-esprima/echojs-dialect `defaults` expressions — + unreachable via acorn but reachable through echojs post-desugar trees. + The scan now covers `defaults`, treats dialect `rest` as a parameter, + and collects param BINDING names via pattern leaves (an ES6 default's + right-hand side is an expression, not a binding). Pinned with a + hand-built dialect tree (a default-closure writing a later var now + computes, instead of silently dropping the write). + +Final harness numbers at c3d1aed (all lanes): corpus 46 — node 37 exact ++ 3 membership + 6 visible skips, 0 divergences; containment 1935 +checks, 0 violations; ejs 32 ok / 1 N/A (arith-basic.js, esprima `**`) / +7 known / 0 new / 0 stale. Suite 268. + +## Review round 4 (residual: binding-split re-declarations, maam dfa8eb1) + +The R1 identifier-declared exclusion assumed the modeled path owned such +names, but a later PATTERN re-declaration of a hoisted-captured name +fresh-binds while the closure writes the pre-minted address — a binding +SPLIT, silently wrong with zero counters (reviewer repro +`var f = function () { a = 5; }; var a = 0; var [a] = [1]; f(); a;` → +{undefined, 1}, real JS 5). Fixed ACCOUNTING-ONLY (the modeled +`captured` rule is unchanged — asserted, and the harness node+containment +numbers are byte-identical): the scan records per-name identifier and +pattern-leaf declaration index lists plus the earliest closure-reference +position, and degrades every split shape — the reviewer repro, the +identifier-only sibling (re-declaration after a non-hoisted capture, +found while generalizing), and pattern-only re-declaration after capture +(the R1 rule now keys on the LAST pattern index). Hoisted-captured +identifier re-declaration stays modeled (pinned: computes, db=0); +all-refs-after-all-declarations shapes stay clean. Suite 269. The +`--types` lane was not re-run: the change adds counters only, no +typeOfNode fact can differ by construction. + +## The `--types` diff lane re-run (oracle facts changed ⇒ re-measured) + +The P3.5 normalizer/machine fixes change what the oracle reports, so the +lane was re-run on the final pin (maam 3e64ca1; work tree assembled from +`//:srcdir-tree` + `//lib:generated` + repo `test/`, conc 4, logs +`~/.cache/maam-p0-logs/P35-types-diff/`). These numbers SUPERSEDE the +Phase 3 figures (67 diamonds / 1319 queries / 1035 unknown) and the +review's interim c69fc81 run (66 / 1306 / 866): + +| files | identical | divergent | N/A | timeouts | +|---|---|---|---|---| +| 458 | 454 (+3 serial re-verifies = 457) | **0** | 1 (tester.js, standing esprima gap) | 3 transient (closure2/4/7, concurrency artifact — each re-verified IDENTICAL serially, same as the review run's 4) | + +Aggregates: **diamonds 69** (baseline 67, interim 66), oracleQueries 1320, +**oracleUnknown 866** (baseline 1035). Reading: the ⊥-receiver fix and +exact string `.length` give the oracle MORE precise facts (unknown down +~16%, two extra diamonds); the hoisted-capture `undefined` widening on +captured vars did not cost a diamond on this corpus. The behavioral gate +is unchanged: zero divergence, flag-off untouched. + +Re-run once more on the round-3 pin (maam c3d1aed, logs +`~/.cache/maam-p0-logs/P35-types-diff-r3/`): **458 files, 457 identical, +0 divergent, 1 N/A (tester.js), 0 timeouts — LANE PASS**, aggregates +byte-for-byte the same (diamonds 69 / queries 1320 / unknown 866): the +R1 accounting and R2 defaults-scan changed no oracle facts on this +corpus. + +# Phase 3.6 gates (typed calling convention / function specialization) + +Date: 2026-07-23. echojs @ eir (this commit), maam @ dfa8eb1 (unchanged +— P3.6 is entirely compiler-side; the oracle interface needed nothing +new: param/return facts come from the existing node-identity +`typeOfNode` over declaration ids and return-argument expressions). +Same environment and color-free protocol as Phases 3–3.5. + +## What landed + +The first deliberate crossing of the unguarded-consumption line — P3.5 +(the differential harness) is the precondition that makes the oracle's +claims trustworthy enough to become facts, and everything that TRUSTS a +claim is fenced by structural, oracle-free machinery: + +- **Local-closed-world escape analysis** (lib/eir/specialize.ts) — + operand flow over the OPTIMIZED lowered EIR, consulting the oracle for + nothing. Two closure-flow shapes qualify: every use of a + make_closure value is a plain call's callee (SSA-visible), or the + closure's single store lands in a promoted, never-exported `%self` + slot whose every load is a plain call's callee (the shape every + toplevel `function` declaration lowers to). ANY other use — edge + arg, object/array member, call argument, construct, return, a + non-promoted (exported) slot, a second store — rejects the function. + A wrong oracle can therefore never widen what specializes. + Slot-load rewrites come in two strengths: same-function loads + dominated by the store, and — when the store sits in the toplevel + ENTRY block with no CALL-effect instruction before it — loads in ANY + function (no user code can run before such a store, so no load can + observe the uninitialized slot; a load textually earlier in the + entry block stays generic, preserving the documented hoisting-lost + throw). The pass runs to a small fixpoint so call sites inside + freshly-lowered clone bodies rewrite too (sum$typed's call of + hypot2 lands on hypot2$typed). +- **Specialized clones, born unguarded** (SpecMode in lib/eir/lower.ts) + — a qualifying function is RE-LOWERED from its AST with an unboxed + signature (`Func.sig`, f64 formals + f64 result): formals arrive raw + and box exactly once at entry, oracle-number arithmetic emits + unbox/f64-op/box straight-line (no diamond, no slow path — "drop the + slow path from the clone" by construction), and `return ` + returns the raw f64. Guards never exist in the clone rather than + being folded out of it. +- **Trust-free post-checks** — the lowered clone is DISCARDED (clone + count telemetry `specRejected`) unless it structurally honors its + sig: env param unused, `this` unused, no frame ops + (arguments/rest/new.target/super), every return operand f64. A lying + oracle that sneaks a capture/`this`/bare-return shape past the type + gate loses the clone, never correctness. +- **Typed direct calls** — new `call_typed` op (imms.direct's typed + sibling): operands `[env, ...raw f64 args]`, callee resolved by name, + re-checked against the callee's sig by the (now module-aware) + verifier: arg types, arity, and the stamped result type must all + match. Exact-arity, non-protected call sites rewrite to + caller-unboxed direct calls (`specSites`); everything else stays on + the generic path (still enumerated — correctness never depends on + rewriting). The emitter gives sigged clones a NATIVE signature — + `double(double...)`: the formals ALONE, since the post-checks + guarantee env and `this` are unused, neither gets an argument slot + (call_typed's EIR-level env operand is simply not emitted; LLVM's O2 + pipeline demonstrably does not dead-arg-eliminate internal functions, + so we do it) — internal linkage, no closure-dispatch interop — which + is what finally lets LLVM inline and scalar-optimize through the + call. +- **Three trust-free optimizer additions** (they fire wherever their + structural proofs hold, oracle or not): `unbox_f64(box_f64(x)) → x` + and `unbox_f64(const n) → f64_const n` annihilation; const-number + edge args admitted into rawJoin conversion as `f64_const` roots-once- + rooted (a loop accumulator seeded `s = 0` finally goes raw — but a + const-ONLY join stays boxed, so flag-off code never grows boxes); and + boolean-join threading (constant true/false edges into a + `to_boolean`+cond_br re-test jump straight to the branch target, + removing the per-iteration `_ejs_truthy` call from typed loop + headers). The threading pass requires the join's param and boolean + to die inside the block — the stage1 matrix caught exactly that + dominance hazard on first run (compiling specialize.js itself), and + the locality check is the fix. +- **Exports are never specialized** in this round, per the plan's ABI + rule; the conservatively-guarded boundary WRAPPER that would dispatch + an escaping/exported function's generic entry to its clone is + deliberate follow-on work (nothing in the P3.6 gate needs it). + +`EJS_NO_EIR_SPEC=1` bisects specialization alone (same mold as +EJS_NO_EIR_OPT). Telemetry rides the `--types` stats line: +`specialized=N specSites=M specRejected=K` (absent when nothing +qualified). + +## EIR-shape unit tests + +//:test-eir green — 130 tests, including the new Phase 3.6 set: the +closed-world clone (sig f64(f64), zero has_tags, zero generic ops, both +sites call_typed, dead closure swept); the wrong-oracle escape trio (a +lying stub oracle types everything {number} while the closure escapes +as a return value / into a LIVE object literal / as a call argument — +zero clones, and the object-literal case documents that a DEAD escape +sunk by the optimizer is correctly no escape); env-capture and +`this`-use clones discarded by the post-checks (specRejected=1 each); +early disqualifiers (bare `return;`, top param, arguments-object); +arity-mismatch sites keeping the generic path while the exact site +rewrites (closure survives for the generic site); the verifier +quartet (f64 entry param without a sig rejected, sigged param +accepted, f64-result function must return raw f64, call_typed checked +against the callee sig: boxed arg / wrong result stamp / unknown callee +all rejected); and the two cleanup passes (unbox-of-const folds to +f64_const; `<`-diamond constant edges thread while the slow arm's +re-test survives). + +## test/types probes + +All ten probes match `node` (the wrongoracle exception unchanged), the +pre-existing five keep their census diamond counts (6/4/5/0/6); census +updated in test/types/README.md: + +- **types-spec1** (new): looping module-local kernel → + `specialized=1 specSites=2`, the extra-arg call site stays generic, + output identical to node and to flag-off. +- **types-spec2** (new): the hypot2-demo shape — hypot2 called only + inside sum, prefix-safe toplevel stores → `specialized=2 specSites=4` + including the site inside sum$typed (the fixpoint round), output + identical to node and to flag-off. +- **types-specescape1** (new): numerically-typed function whose closure + ALSO travels as a call argument → no specialization telemetry (both + the num|str param join and the structural escape reject it), and the + escaped path feeds a string through the generic call — output + identical to node and to flag-off. +- types-wrongoracle1 unchanged: flag-off ≡ --types ("42"), the + cross-module string still routes through the guard. + +## Microbenchmark (types-bench1, deltas vs Phase 3 / 3.4) + +Same kernel, same protocol (7× interleaved, /usr/bin/time -p). The +kernel now compiles to: a specialized `double kernel$typed(double)` +clone whose loop is raw f64 end-to-end (f64 loop-carried params via the +const-root extension, f64_consts, comparison threaded straight to the +branch — no box, no guard, no runtime call in the body), LLVM-inlined +into the toplevel loop (verified in the .bc.opt disassembly: no call +remains, only `.i`-suffixed inlined blocks). + +| build | P3 | P3.4 | P3.6 | +|---|---|---|---| +| flag-off median | 3.19 s | 3.21 s | 3.24 s (3.20–3.29; a concurrent-load re-run read 3.33–3.40 with --types unchanged) | +| --types median | 0.31 s | 0.23 s | **0.07 s** | + +**Speedup ~46× median (was 14.0×)** — the typed runtime dropped another +70%, and at ~1.75 ns per iteration the loop is at fdiv-throughput +territory; the remaining wall time is the boxed outer loop's slot +traffic, which is shapes-and-layouts (P4) territory, not calls. +diamonds=9, oracleUnknown=0, `specialized=1 specSites=1`, output +identical (13333303333341514000). + +## hypot2 demo (deltas vs Phase 3.4 — the P3.6 flagship shape) + +The demo Phase 3.4 could not move (dominated by the boxed +call/closure/loop overhead around hypot2) is exactly what P3.6 exists +for. `--types` stats: diamonds=7 oracleQueries=31 oracleUnknown=0 +**specialized=2 specSites=3** — hypot2 AND sum clone; the toplevel +`sum(20000000)` call, hypot2's call inside generic sum, and hypot2's +call inside sum$typed (fixpoint round) all rewrite to call_typed. +hypot2$typed is three raw float ops and a raw return; sum$typed is a +raw-f64 loop calling it directly. At the LLVM level NO definition or +call of either clone survives — hypot2$typed inlined into sum$typed +inlined into the toplevel, the hot loop is five raw float ops with +`phi double` accumulators, boxing once at the console.log boundary +("the demo's hypot2 inlines into its caller's loop and the box/unbox +pairs annihilate", as the plan wrote it). + +| build | P3/P3.4 (3 runs) | P3.6 (7× interleaved) | +|---|---|---| +| flag-off | 2.5–2.7 s | median 2.69 s | +| --types | 0.33–0.51 s (~7×) | **median 0.03 s (~90×)** | + +Output identical to node and flag-off (5.333333333333098e+21, n=20M). +Full before/after EIR and LLVM excerpts regenerated in +`~/src/echojs/hypot2-types-before-after.txt` (Phase 3.4 and Phase 3 +records preserved below the new section). + +## The --types diff lane (behavioral gate) + +Re-assembled work tree (srcdir-tree + lib/generated + repo test/, plus +the hypot2 demo.js), conc 4, logs `~/.cache/maam-p0-logs/p36-lane-final/`: + +| files | identical | divergent | N/A | timeouts | +|---|---|---|---|---| +| 459 (458-file corpus + demo.js) | 458 | **0** | 1 (tester.js, unchanged) | 0 | + +Aggregates: **diamonds 76** = the P3.5 baseline 69 + demo.js's 7; +oracleQueries 2997, oracleUnknown 2191 (up from 1320/866 because the +specialization pass now queries per-candidate param/return nodes — +telemetry, not a behavior change). An earlier run of the same corpus +WITHOUT demo.js, before the cross-function extension, read 458/457/0/1 +with diamonds 69 byte-identical to the P3.5 baseline. + +## Matrix + stage2 ≡ stage3 (flag off) + +Full matrix on the final code: //:test-eir, //:test-eir-lowtier, +//:test-stage0..3 in one build — BUILD SUCCEEDED (exit 0). The FIRST +matrix run of this phase FAILED, by design of the gate: stage1 +(compiling lib/eir/specialize.js itself, flag-off) hit an EIR verifier +dominance error — boolean-join threading had bypassed a join whose +param was still consumed downstream. The fix (the join's param and +boolean must die in-block) is pinned by the re-run; flag-off behavior +is covered by the stage corpus gates, and the only new flag-off-capable +pass (threading) is semantics-preserving constant-edge routing. + +## Reading + +P3.6 holds the phase's core promise: oracle claims become facts ONLY +inside a fence of structural evidence (escape analysis, post-checks, +sig-aware verification) that a wrong oracle cannot cross, and the first +matrix run proving the fence catches real hazards (the threading +dominance bug) is exactly the discipline paying off. The demo kernel's +call boundary is gone — specialized, direct, native-signature, inlined +— and the ~46× ceiling now sits where the plan predicted the next wall: +boxed heap traffic (shapes, Phase 4) rather than calls or arithmetic. diff --git a/docs/maam-plan.md b/docs/maam-plan.md new file mode 100644 index 00000000..fe77bc45 --- /dev/null +++ b/docs/maam-plan.md @@ -0,0 +1,574 @@ +# MAAM integration plan: a type oracle for EIR + +How the `echojs-maam` abstract interpreter (submodule at +`external-deps/echojs-maam`, branch `ejs-integration`) feeds types into EIR +lowering, in independently-landable phases, without restructuring either +codebase. + +## Context: what echojs-maam is, as found + +`echojs-maam` ("maam-fable") is a ~7k-line TypeScript transliteration of +Darais/Might/Van Horn's MAAM — one definitional CESK* interpreter +(`src/lang/machine.ts`) run under different monads to get concrete evaluation, +k-CFA, and path/flow/flow-insensitive analyses. It consumes **standard ESTree** +(`analyze(program, spec)` in `src/analysis.ts`; zero runtime deps — acorn is +dev-only, behind `src/lang/parse.ts` which is deliberately excluded from the +build). The `ejs-integration` branch already models the shapes echojs's +pre-EIR desugars emit: `%objectCreate`, `%setPrototypeOf`, +`%setConstructorKind*`, `%constructSuper`, and the +`Object.defineProperty` method/accessor patterns (`test/echojs-shape.test.ts` +hand-builds exactly those trees). 192 tests pass under `npm test`. + +It is research-quality and honest about it: exceptions are control-only, +cross-module linking is unmodeled (imports degrade to `undefined`), calls to +unmodeled externals degrade and are *counted* (`metrics.unknownCalls`), and the +Octane corpus shows the heavier benchmarks need the widening knobs +(`stateCap`/`shapeCap`) to converge. What it computes is exactly what we want: +type-aware hidden classes per allocation site (`result.layouts()` — field +names, `TypeSig`s, struct offsets), per-function `(param types) → return type` +tables (`result.specializations()`), accessor-dispatch sites, and — via +`concreteEval()` — a genuine concrete interpreter usable as a differential +oracle. What it does **not** yet have is a per-AST-node type query: +`valueOfVar(name)` joins by *name* across all configs, and core locations map +only to source *spans* — which echojs trees don't carry (see mismatches below). +That query is the main new surface this plan adds, on the maam side. + +On the echojs side the seams already exist by design: `lib/eir/ops.ts` is the +declared effect-table contract ("the optimizer and the abstract interpreter"), +including an unused low tier (`has_tag`, `unbox_f64`, `box_f64`, +`f64_add/sub/mul/div/lt`); `lib/eir/scopes.ts` keys its `refs` map on AST nodes +(`Map`); `lib/eir/lower.ts` lowers `BinaryExpression` in +one place (`LowerFunction.binary`, the `binops` table) and every variable +read/write through `readVariable`/`writeVariable`; `Inst.type` in +`lib/eir/ir.ts` is an `"any"` placeholder awaiting the lattice. Note: +`lib/eir/emit.ts` does **not** yet implement the low-tier ops — that is a +prerequisite phase, pure echojs work. + +## ESTree dialect mismatches (echojs `lib/estree.ts` vs. what maam reads) + +Found by inspection; the adapter (Phase 0) must handle each: + +1. **`TryStatement.handlers` (array) + `guardedHandlers`** vs. standard + `handler`. `normalize.ts:313` reads `s.handler` — echojs trees would + silently drop every catch clause. Fix in maam: accept both. +2. **No `range`/`start`/`end` on nodes.** echojs parses with + `esprima.parse(src, {loc: true, raw: true})` (`lib/passes/gather-imports.ts:275`); + maam's `spanOf` falls back to `{0,0}`. Consequence: spans cannot key + anything; the oracle must be **node-identity** keyed (same tree, in + process). Synthetic desugar nodes have no positions at all. +3. **Functions carry `defaults`/`rest`** (old-esprima style) instead of + `AssignmentPattern`/`RestElement` params. maam must evaluate `defaults` + (echojs EIR handles them natively; the analysis must match). +4. **`MetaProperty.meta/property` are raw strings**, not Identifiers — moot + post-desugar (`DesugarMetaProperties` removes them). +5. **Toplevel wrapper**: at `collectEIRToplevel` time the Program body is one + synthetic `FunctionDeclaration` (from `insert_toplevel_func`) whose body + holds the module statements, including `ImportDeclaration` / + `ExportNamedDeclaration` wrappers. The adapter analyzes + `{type:"Program", body: toplevel.body.body}` (preserving node identity) + and must tolerate export wrappers inline. +6. **Intrinsic coverage gap**: echojs's whitelist (`lib/eir/intrinsics.ts`) + includes `%arrayFromSpread`, `%constructSuperApply`, `%constructApply`, + `%getNewTarget`, `%makeGenerator`/`%generatorYield`/…, + `%createIteratorWrapper`; maam models only the class/prototype set. Unknown + intrinsics must degrade *soundly* (to ⊤, see below), never throw. + +## The interface contract + +echojs side, new file `lib/eir/oracle.ts` (the only new echojs surface): + +```ts +// what lowering consumes; deliberately smaller than what maam computes +export type TypeTag = "number" | "string" | "boolean" | "undefined" + | "null" | "object" | "closure"; +export interface EirType { + tags: ReadonlySet | "top"; // "top" = no information +} +export interface TypeOracle { + // type of the value an expression node evaluates to (join over all + // reached contexts); "top" when unknown/unanalyzed + typeOfNode(n: e.Node): EirType; + // true iff metrics.unknownCalls === 0 — required before any + // UNguarded consumption (guarded fast paths don't need it) + closedWorld(): boolean; + describe(): string; // stats line for --types logging +} +``` + +maam side (`ejs-integration` branch), additions to `AnalysisResult`: + +```ts +// node-identity keyed; built by having normalize.ts record the source +// node (not just its span) at the same points it records siteSpans, +// plus declaration-node → alpha-renamed core Name for bindings +nodeTypes(): ReadonlyMap; // e.g. "num", "num|str", "⊤" +typeOfNode(n: estree.Node): TypeSig | undefined; +``` + +plus one semantic fix: **degrade to ⊤, not `undefined`**. +`machine.ts` currently binds unknown-call results and unmodeled imports to +`domain.lit(litUndef)` (`machine.ts:1296,1355`) — fine for reachability, +**unsound as a type** ("this is undefined" vs. "this is anything"). Add a +`domain.top` to `ValDomain` and use it in `degrade`. This is the one +non-additive maam change and it lands first. + +## Phases + +**Phase 0 — plumbing probe (consume nothing).** +Add `--types` to `lib/options.ts` (default off). When on, `compile()` in +`lib/compiler.ts` — after `pre_eir_convert`, before `collectEIRToplevel` — +calls a thin adapter (`lib/eir/oracle.ts`) that imports maam, wraps the +toplevel body as a Program, runs `analyze(prog, kCFA(1, "flow-sensitive", +"call-site", /*shapeCap*/ 64, false, false, false, /*stateCap*/ 512))`, +and logs `result.describe()` + `metrics` + wall time. Nothing downstream reads +it; a crash or `RestrictionError`/`NormalizeError` degrades to a warning, never +a compile error. maam-side deliverables: `handlers` shim, `defaults`/`rest` +handling, unknown-intrinsic tolerance. The real point: **measure** whether +compiler-sized modules converge and at what cost, on our actual sources. +*Gate:* full matrix green with flag off (`buck2 build //:test-eir +//:test-stage0 //:test-stage1 //:test-stage2 //:test-stage3`); stage2≡stage3 +per the functional gate under "Validation strategy" (raw binary byte-identity +does NOT hold today even on pristine HEAD — buck stages link in per-genrule +temp dirs, so LC_UUID/embedded-path/signature metadata differs; discovered +during Phase 0); `ejs --types` over `test/*.js` and `lib/*.ts`'s generated JS +reports stats without crashing — run via the node-hosted dev tree: +buck-staged work trees contain no `external-deps/`, so `--types` there +warns-and-skips by design. + +**Phase 1 — the node-keyed oracle.** +maam: ⊤-degradation; `normalize.ts` records `Loc → estree.Node`; +`analysis.ts` exposes `nodeTypes()`/`typeOfNode()`; unit tests in maam's suite +(node-identity round-trip through hand-built echojs-dialect trees, extending +`test/echojs-shape.test.ts`). echojs: `TypeOracle` adapter mapping `TypeSig` +strings to `EirType`; `--types` now also prints per-binding types for a +`--types-dump` debug flag. Still consumes nothing in codegen. +*Gate:* matrix green (flag off); new maam tests green; oracle dump on +`test/eir-toplevel1.js` matches hand-checked expectations. + +**Phase 2 — emit the low tier (echojs only, independent of maam).** +Implement `has_tag`, `unbox_f64`, `box_f64`, `f64_add/sub/mul/div/lt` in +`lib/eir/emit.ts` (NaN-boxing checks mirror `LLVMIRVisitor.isNumber` in +`lib/compiler.ts`); teach `lib/eir/verifier.ts` that f64/i1-typed values may +only flow into their consumers (`Inst.type` gets its first real values: +`"f64"`, `"i1"`); unit tests in `lib/eir/tests.ts` (`buck2 build //:test-eir`) +via hand-built `FunctionBuilder` functions asserting printer/verifier/emit +behavior, plus one end-to-end test file exercising a hand-forced fast path. +*Gate:* `//:test-eir` green; full matrix green (no lowering changes yet). + +**Phase 3 — typed arithmetic, born in lowering, guarded, flag-gated.** +`LowerFunction.binary()` consults the oracle (threaded through +`lowerAnalyzedFunction` from `collectEIRToplevel`; `null` oracle = today's +behavior). When both operands' types ⊑ number for `+ - * / <`, emit the +guarded diamond (same block-splitting shape as `LowerFunction.logical()`): +`has_tag` both → fast block `unbox_f64/f64_op/box_f64` → join blockparam; +slow block keeps the generic op. **Guarded consumption is correct even if the +oracle is wrong** — the guard decides at runtime; only code size/speed change. +Unguarded (guard-free) emission stays out until `closedWorld()` plus much more +validation. Why born-typed rather than a post-hoc `optimize.ts` pass: the +rewrite needs CFG surgery (block split + join params), which lowering already +does idiomatically, while `optimize.ts` is a flat in-place scanner — a post-hoc +pass would be *more* code, not less. (A post-hoc pass remains attractive later +for typing *optimizer-created* values; nothing here precludes it.) +*Gate:* matrix green flag-off, stage2≡stage3 functional gate; a `--types` lane: +compile the full `test/` suite with `--types` under the node-hosted compiler +and diff every output against the flag-off baselines (byte-identical stdout); +`test/modernization/`-style probe discipline for a new `test/types/` dir +(each file diffed against `node `); an arithmetic microbenchmark +demonstrating the fast path fires. + +**Phase 3.4 — diamond pre-work (trust-free optimizer passes).** +Two passes that pay off on the Phase 3 diamonds immediately and change +nothing about the trust story (guard-borne correctness holds; a wrong +oracle still only costs speed), visible in the Phase 3 demo dumps: +(a) **dominated-guard elimination** — `hypot2`'s first diamond tests +`has_tag %2` twice, and later diamonds re-test values already proven +number on the fast edge; merging dominated guards turns three diamonds +into one guard region with one slow path; (b) **f64 block params for +optimizer-created joins** — a controlled lift of the P2 +raw-values-cannot-cross-blocks rule, scoped to joins the optimizer +itself builds, so fast regions compute unboxed end-to-end and box once +at the region exit — kills the bits_alloca round-trips the Phase 3 +benchmark flagged as headroom. Sequenced before P3.5 because it needs +none of it. + +**Phase 3.6 — typed calling convention / function specialization.** +The Phase 3 diamonds keep every call boxed and re-box at every join; the +remaining order of magnitude lives here. For a function with a **local +closed world** — the closure value never escapes (not exported, never +stored through a degraded write, never an argument to an unknown call: +all trackable as operand flow), every call site enumerated in-module, +argument types proven at each (`specializations()` already computes the +per-function `(param types) → return type` tables; so far unconsumed) — +emit a specialized clone with an unboxed signature (`f64(f64, f64)`-class), +rewrite known call sites to direct calls that unbox at the caller, and +drop the slow path from the clone. **Module-level exports are never +specialization candidates** — not even with whole-program analysis: +the slot-based module ABI exposes boxed ejsvals to both JS and native +consumers, and specializing an export's signature would break that +contract. Instead the export's generic boxed entry is *conservatively +typed* and carries Phase-3-style guards at the boundary — exactly the +hypot2-shape diamonds — dispatching to the specialized clone when they +pass and keeping the generic path otherwise. The same wrapper shape +serves any function that escapes locally. This is the point where LLVM +finally gets to inline and do real scalar optimization (the demo's +`hypot2` inlines into its caller's loop and the box/unbox pairs +annihilate) — with the boxed world intact at every ABI boundary. + +This crosses the unguarded-consumption line the plan drew: oracle claims +become facts, so **P3.5 (the differential harness) is a hard +precondition**, and per-function local-closed-world evidence replaces the +too-blunt global `closedWorld()` (console.log alone fails that in 90% of +modules). Mechanical prerequisites in EIR: typed function +signatures (a controlled lift of the P2 raw-values-cannot-cross-blocks +rule at specialized function boundaries), a direct-call op carrying the +specialized symbol (`imms.direct`'s typed sibling), and static callee +checks (no `arguments`/rest/defaults/`this` in the clone). GC is +indifferent (conservative scan tolerates raw doubles; false retention +only). + +Pre-work lives in Phase 3.4 (trust-free, sequenced first). Longer-term +this phase dovetails with the IR-in-manifest direction (2026-07-08): +cross-module ANALYSIS through manifests can widen which internal calls +are provably typed — but the export-boundary rule above stands +regardless; manifests inform the guards, they don't remove the boxed +ABI. + +**Phase 4 (outline only) — shapes.** +`result.layouts()`/`constructors()` give monomorphic allocation sites with +struct offsets. Consuming them (fixed-offset property access) needs a shape +guard op and runtime object-layout support that don't exist; design that as +its own document once Phase 3 has proven the pipeline. Until then, shapes +inform *diagnostics* only (polymorphism warnings under `--types-dump`). + +## Self-hosting strategy + +The constraint: stage1+ compilers are the compiler compiled by itself, and the +esprima fork parses ES6-era JS only. maam's own source is strict TS 5.x using +`??`, `?.`, and generators; `tsc` at `target: ES2022` leaves `??`/`?.` in the +output, which the esprima fork cannot parse — so maam **cannot run under a +self-hosted compiler today**. Options weighed: + +- **(a) Vendored babel-downleveled build** — precedented (docs/plans.md + proposes exactly this for parser un-forking), and `//lib:generated` already + runs babel. Viable, but it drags a second-build-of-a-submodule into the + bootstrap now, for zero benefit while the flag is off. +- **(b) Node-only analysis, flag off during bootstrap** — stages remain + byte-identical trivially (the flag is off everywhere in the matrix); `--types` + is available wherever the compiler runs under node (stage0 and dev use). +- **(c) Syntax-downlevel pass in echojs** — that's the modernization project + (`test/modernization/`, 13 parser gaps), not this one. + +**Recommendation: (b) now, (a) when promotion is wanted.** Concretely: Phase 0 +imports maam via a `tsc -p tsconfig.build.json`-built `dist/` (an ESM/CJS +interop wrinkle exists — maam is `"type": "module"`, the babel'd compiler tree +is CJS under node 22.4; a `tsconfig.cjs.json` variant in the maam repo is the +one-file fix). Promotion to self-hosted `--types` waits until either the +babel-vendored build (a) or the TS port + parser modernization make it moot. +Until promotion, `--types` in a stage1+ compiler is a no-op with a warning. + +## Validation strategy + +- **Bootstrap matrix, every phase:** `//:test-eir`, `//:test-eir-lowtier` + (standalone — must be named explicitly; stage-green does not imply it + ran), `//:test-stage0..3`. + stage2≡stage3 is a *functional* gate, not raw byte-identity (which fails on + pristine HEAD from link metadata alone): stage2 and stage3 binaries, run in + identical work dirs over a fixed corpus, must produce byte-identical + outputs, and any stage2-vs-stage3 binary diff must be attributable to link + metadata (`cmp` after `codesign --remove-signature` + masking LC_UUID, or + diff the `--leave-temp` .ll artifacts). Executable byte-compares are only + meaningful when both binaries were linked in the same directory. Flag-off + means MAAM cannot regress the matrix. +- **Concrete interpreter as differential oracle:** `concreteEval()` is real + and exact (`analyze(prog, concreteEval()).result`). Add a maam-repo harness + that runs closed-world test files (start with `intrinsics: true` to cover + `Math`/`Array`/`parseInt`) and diffs the final value against `node` — any + divergence is a machine bug that would poison the abstract results too. + Precondition per file: `metrics.unknownCalls === 0`, else skip (degradation + makes the diff meaningless). Also diff against `ejs`-compiled output for the + subset both support — that checks *echojs* too, for free. +- **Abstract-vs-concrete containment spot checks:** for files the concrete + interpreter handles, assert the k-CFA `typeOfNode` at each checked node is ⊒ + the concrete value's type — cheap soundness fuzzing, catches ⊑-direction bugs. +- **`lib/eir/tests.ts`:** low-tier emit/verify (Phase 2), oracle-driven + lowering shape (Phase 3: assert the printed EIR contains + `has_tag`/`f64_add` diamonds for a numeric snippet, and does not for a + string one). +- **`--types` diff lane (Phase 3 gate):** entire `test/` suite compiled with + and without `--types`; outputs must be byte-identical. + +## Risks and unknowns + +- **Convergence on compiler-sized inputs.** Octane's heavier files don't + converge in tens of seconds; `lib/compiler.ts`'s generated JS is bigger. + `stateCap`/`shapeCap` bound time but cost precision. Phase 0 exists to turn + this unknown into a number before anything depends on it. +- **Degradation soundness.** Unknown calls/imports currently read as + `undefined`; consuming that as a type would miscompile. Fixed in Phase 1 + (⊤-degradation) and defended in depth by guarded-only consumption. +- **Unvalidated soundness claims.** The analyzer's soundness is asserted by + its own tests, not proven against ejs semantics (e.g. ejs's no-TDZ let/const, + `to_boolean` purity). Guards make Phase 3 immune; anything unguarded needs + the differential harness first. +- **Node-identity coupling.** The oracle keys on the exact post-desugar tree + object; any future pass that clones nodes between analysis and lowering + silently drops types (fail-soft to ⊤, but worth a debug counter). +- **ESM/CJS interop** for importing maam's build from the babel'd tree + (node 22.4 pinned in CI — no `require(esm)`). +- **Two-repo coordination.** The submodule pin advances with the oracle API; + phases state which repo each deliverable lands in to keep either repo + releasable alone. + +## Future work: can this architecture become production-grade? + +Assessment (Claude, 2026-07-11), recorded here so the Phase 0 numbers get read +against an explicit hypothesis rather than vibes. + +**The pessimistic reading is correct about the engine.** Small-step monadic +AAM/CESK* is close to the most expensive known way to compute a flow analysis: +every step pays monad plumbing, the state space is the product of +control × store × continuation abstractions, and the caps that force +convergence (`stateCap`/`shapeCap`) buy termination by discarding exactly the +precision we want to consume. The industrial abstract interpreters that ship +(Infer, Astrée) are compositional/summary-based engines, not small-step +machines. "This exact machine, flow-sensitive, over 100k-line modules, in +seconds" is not a realistic endpoint, and no micro-optimization changes that. + +**But the deployment profile is unusually favorable**, which is why the +architecture is worth keeping anyway: + +1. *AOT oracle, not IDE/CI.* Offline, deterministic, whole-program, behind an + opt-in flag — seconds-to-a-minute of compile time is tolerable. +2. *Wrong answers cost speed, not correctness.* Guarded consumption makes + precision an optimization, not an obligation. +3. *The input language is tiny.* Not "JavaScript" — the post-desugar echojs + dialect: no TDZ, whitelisted intrinsics, no eval/with/dynamic loading. + +**The durable asset is the definitional machine as reference semantics.** +`concreteEval()` as a differential oracle is something hand-optimized +analyzers never have. The classic path from research analyzer to product is +exactly this split: keep the slow, obviously-correct machine as the spec, and +if (and only if) Phase 0 measurements demand it, grow a fused fixpoint engine +— worklist, flow-insensitive-then-refine, or per-function summaries +(`specializations()` already gestures at summaries) — that shares the domain +definitions and is continuously diffed against the reference. Rewrite the +fixpoint loop, never the semantics. + +**The `TypeOracle` interface is the insurance policy.** It is deliberately +smaller than what maam computes and keyed on nodes, not maam internals. If +convergence on compiler-sized inputs is unacceptable, the engine behind +`typeOfNode()` is swappable — a monovariant Andersen-plus-type-lattice pass +would cover the Phase 3 arithmetic use case at a fraction of the cost — and +lowering never knows. Nothing should be built that assumes the MAAM machine +specifically sits behind the oracle. + +**Decision rule:** let the Phase 0 measurement, not aesthetics, make the call. +Three outcomes: (a) converges with acceptable cost on our corpus → ship as-is +behind `--types`; (b) converges only with heavy widening → keep it for +diagnostics/differential duty, start the fused engine sharing its domains; +(c) doesn't converge → oracle interface stays, engine is replaced outright. + +Smaller forward items surfaced by the Chunk A integration review: + +- `metrics.unknownCalls` was broadened during integration (method/apply/ + tailcall degradations now count, not just call/new). This is what the + Phase 1 `closedWorld()` contract needs, but it changes the metric's + definition out from under the numbers in the maam repo's docs/paper — + regenerated tables will shift, and any comparison must say so. +- Rest parameters are degraded (bound to an empty abstract array + counted), + not modeled. Precise rest needs a core/machine varargs extension — a + natural Phase 1 companion to ⊤-degradation. +- Guarded-catch guards are modeled per-clause; cross-clause guard side + effects (guard 1 mutates, clause 2 observes) are dropped. Unreachable from + echojs output (esprima always emits `guardedHandlers: []`) — revisit only + if that changes. +- `tryIntrinsic` dispatches on the `%` name prefix with a `scope.has()` + escape for bound names (`%super`). If echojs ever grows more bound + `%`-names or direct `%super(...)` calls, the intrinsic whitelist in both + repos needs to stay in sync — a shared fixture file is the eventual answer. + +## What we explicitly will NOT do + +- No parser swap or syntax modernization as part of this (tracked separately + in docs/plans.md). +- No runtime changes beyond what the already-declared low-tier ops need; no + new object layouts, no shape guards, no GC work. +- No rewrite or restructuring of maam's monad/driver machinery, and no + EIR-targeting maam frontend (it keeps consuming ESTree; the ANF core stays). +- No post-hoc "type inference pass" duplicated inside echojs — types come from + the oracle or stay `any`. +- No default-on behavior anywhere until the differential harness and the + `--types` diff lane have real mileage. + +## Phase checklist (for /goal sessions) + +Ids are `maam-P#` (formerly bare P0..P4 in this doc and in commit +messages/results docs). + +- [x] **maam-P0** `--types` flag + `lib/eir/oracle.ts` adapter + maam dialect shims + (`handlers`, `defaults`/`rest`, unknown-intrinsic tolerance, toplevel + unwrap); stats logging only. + *Done 2026-07-19* (maam 1bea5de; echojs d3e3fd1): all gates green, + numbers in docs/maam-p0-results.md. Headline finding: convergence on + compiler-sized modules is STILL OPEN — blocked on maam normalizer + coverage (TemplateLiteral/ForOfStatement/destructuring params = 100% + of the compiler-module rejects), not on the engine; where analysis + runs, it converges with zero timeouts and analysis cost is noise next + to codegen. + *Gate:* full matrix green (flag off); `--types` runs over `test/*.js` + without crashing (node-hosted dev tree — buck work trees have no + `external-deps/`, so `--types` there warns-and-skips by design); + convergence/timing numbers recorded in the PR. +- [x] **maam-P1** maam: ⊤-degradation + `nodeTypes()`/`typeOfNode()` (node-identity + keyed); echojs: `TypeOracle` + `--types-dump`. Per the P0 results, P1 + should FRONT-LOAD maam normalizer coverage for TemplateLiteral, + ForOfStatement, and destructuring/defaults/rest params (these block + every compiler-sized module), surface cap-hit counters in `metrics` + (saturation is currently unobservable), then re-run the P0 measurement + to close the convergence question. + *Done 2026-07-21* (maam 8d6a157; echojs this commit). Convergence + question CLOSED: outcome (a) — compiler-sized modules converge + naturally (0 stateCap hits; self-compile analyzes 44/45 modules, sole + remainder lib/runtime's non-literal defineProperty key); numbers in + docs/maam-p0-results.md. Oracle contract notes: `typeOfNode` on a + node mapped to a declared variable reports the join over the + variable's whole lifetime (reassignment-widening — sound, not + value-at-site); spliced/shared node objects are poisoned to + `undefined` (consumer degrades to ⊤); `closedWorld()` requires BOTH + `unknownCalls` and `degradedBindings` zero. Known ⊤ classes on real + trees: unmodeled imports, unknown intrinsics/method calls + (intrinsics=false), unreached code, and array patterns in ALL + positions (declaration, param, assignment) — DesugarDestructuring + routes every array pattern through `%createIteratorWrapper` before + the probe, so maam's native pattern paths are exercised only by its + own tests; modeling that intrinsic (or reordering the desugar) is the + obvious next precision win for P2/P3. + *Gate:* maam suite green (incl. new node-identity tests); matrix green; + hand-checked oracle dump for `test/eir-toplevel1.js`. +- [x] **maam-P2** emit + verify `has_tag`/`unbox_f64`/`box_f64`/`f64_*`; + `Inst.type` carries `"f64"`/`"i1"`. + *Gate:* `//:test-eir` green with new low-tier tests; matrix green — + the matrix line now includes `//:test-eir-lowtier` (standalone + target; it does NOT ride the stage targets), which compiles + test/eir-lowtier1.js plain + under `EJS_EIR_LOWTIER=1` injection and + asserts output parity, binary divergence, and per-op IR presence + (fadd/fsub/fmul/fdiv/fcmp olt) — also a stale-llvm.node detector. + *Done 2026-07-21.* Notes: node-llvm needed new FP bindings + (createFSub/FMul/FDiv/FCmpOLT — only FAdd existed); `has_tag + "number"` delegates to LLVMIRVisitor.isNumber (icmp ult against + EJSVAL_SHIFTED_TAG_INT32 — the int32 tag exists in the ejsval layout + but is never minted, and the threshold excludes it, so + unbox-as-raw-double is safe by construction); raw f64/i1 may NOT + cross block boundaries as edge args (Phase 3 diamonds rejoin boxed); + cond_br accepts i1 or legacy "any" conditions. Runtime backlog item + found: `_ejs_op_div` aborts EJS_NOT_IMPLEMENTED on non-number LHS + (ejs-ops.c ~901) — sub/mul coerce, div doesn't. +- [x] **maam-P3** oracle-guided guarded arithmetic in `LowerFunction.binary`, + `--types`-gated. + *Gate:* matrix green + stage2≡stage3 functional gate (flag off); + full-suite `--types` + diff lane byte-identical; EIR-shape unit tests; microbenchmark delta + recorded. + *Done 2026-07-22* (lowering 568efc7; gates this commit). Diamonds for + `+ - * / <` on exact-{number} operands (literals special-cased; + widened unions decline); correctness is guard-borne — proven at + runtime with a wrong oracle (cross-module valueOf-throw → slow path + → caught). Gates: diff lane 458 files, 457 identical, 0 divergent + (independently reproduced 514/514 on a superset), 67 diamonds + suite-wide; test/types/ probe dir documents firing and declining + shapes incl. the wrong-oracle keystone; microbenchmark 10.3× median + on a pure-numeric kernel (3.19s → 0.31s, diamonds=9) — best-case + ceiling, not suite expectation; full matrix + functional stage2≡stage3 + green. Numbers in docs/maam-p0-results.md "Phase 3 gates". The lane + script fails on zero-files-compared and zero-diamonds (vacuous-pass + guards from review). +- [x] **maam-P3.4** diamond pre-work, trust-free (see the Phase 3.4 section): + dominated-guard elimination + f64 block params for + optimizer-created joins. Landed as lib/eir/optimize-guards.ts: + proven-number guard folding (dominator-tree facts + value-intrinsic + proofs), structural guard-region merging (hypot2's three diamonds → + one region, one slow path), and rawJoin f64 params — an explicit + per-param marker the verifier re-checks in full (f64 params REQUIRE + it; every incoming arg must be f64; catch/unwind excluded), so + lowering-created edges keep the strict P2 boxed rule. + *Gate:* matrix green; --types diff lane still byte-identical; + EIR-shape unit tests (merged guard region; unboxed fast region + boxing once); types-bench1 + the hypot2 demo re-measured, deltas + vs the Phase 3 baselines recorded in docs/maam-p0-results.md + "Phase 3.4 gates". +- [x] **maam-P3.5** differential harness in maam repo (`concreteEval` vs node vs + ejs on closed-world tests) wired into its CI. + *Gate:* zero divergences on the curated corpus. + Landed as maam test/differential/ (`npm run diff-harness`, in maam CI): + 46-file corpus, node lane 37 exact + 3 membership (documented machine + over-approximations) + 6 visible skips, 0 divergences; containment + lane 1935 node checks against the oracle spec and its intrinsics twin, + 0 violations; ejs lane 32 ok / 1 N/A / 7 known-divergent — seven + root-caused PRE-EXISTING echojs bugs (typeof null, -0===0, + Math.round(-2.5), Number whitespace, negative >>>, `1+null` runtime + abort, esprima `**`) pinned in ejs-known-divergences.json with + stale/unvalidatable-entry accounting. Building the harness required + making `intrinsics: true` actually exact under the concrete domain + (it silently degraded before) and fixed the machine/normalizer bugs + the diff surfaced — chiefly a closure-capture unsoundness (writes to + later-declared same-scope vars silently dropped; generalized to + function expressions/arrows/methods after adversarial review) — + details in docs/maam-p0-results.md "Phase 3.5". +- [x] **maam-P3.6** typed calling convention / function specialization + (see the Phase 3.6 section; HARD PRECONDITION: P3.5 green): + local-closed-world escape analysis, specialized unboxed clones + + direct calls; exports are NEVER specialized (boxed slot ABI is a + contract with JS and native consumers) — their generic entries get + conservatively-typed boundary guards dispatching to the clone. + *Gate:* matrix green; --types diff lane still byte-identical; + wrong-oracle probes extended to the specialization path (a + function that LOOKS closed-world but isn't must be provably + rejected by the escape analysis, not miscompiled); demo-class + benchmark showing the clone inlines (delta vs the Phase 3 10.3× + ceiling recorded). + Landed as lib/eir/specialize.ts (+ SpecMode clone lowering in + lower.ts, Func.sig/call_typed/f64_const in the IR, sig-aware + verifier + native-signature emission): structural, oracle-free + escape analysis over lowered EIR (SSA-visible closures and + single-store promoted %self slot cells; ANY other flow rejects), + trusted-mode clone lowering (no diamonds, no slow paths, f64 + formals boxed once at entry, raw f64 returns), trust-free + post-checks discarding any clone that can't honor its sig + (env/this use, frame ops, non-f64 returns), exact-arity call + sites rewritten to call_typed with caller-side unboxing — + same-function store-dominated slot loads, plus cross-function + loads when the store sits in the toplevel entry prefix (no + CALL-effect inst before it), to a fixpoint so sites inside + freshly-lowered clones rewrite too. Three trust-free optimizer + additions let clones go raw end-to-end: unbox(box)/unbox(const) + annihilation, const-number rawJoin edge roots (f64_const), and + boolean-join threading. Exports/escaping functions are simply + never specialized in this round — the boundary-guard wrapper + dispatching escaping entries to the clone remains OPEN follow-on + work (nothing needs it for the gate). + Gates: matrix green; diff lane 0-divergent; wrong-oracle + specialization probes at unit level (lying stub oracle vs + escaping shapes → 0 clones) and probe level + (test/types/types-spec1/2.js, types-specescape1.js); types-bench1 + 0.23 s → 0.07 s (14.0× → ~46× vs flag-off); the hypot2 demo goes + ~7× → ~90×, both clones verified LLVM-inlined into the toplevel + loop (before/after regenerated in + ~/src/echojs/hypot2-types-before-after.txt). Details in + docs/maam-p0-results.md "Phase 3.6 gates". +- [x] **maam-P4** (design doc only) shape-guarded property access: guard op, + runtime layout, promotion criteria from Phase 3 experience. + Delivered as **docs/shapes-plan.md** (2026-07-23): type-aware + runtime shape tree mirroring maam's classes 1:1 (representation in + the class identity — a passed guard proves offset AND repr, feeding + the P3 raw-f64 machinery), slot-array object layout with + dictionary-mode fallback for every exotic path, has_shape/ + slot_load/slot_store/make_object_shaped EIR ops with the verifier's + new effect-kill soundness class, node-identity oracle queries + (layoutOfNode/constructorReportOfNode/receiverShapesOfNode) as the + maam-side prerequisite, promotion criteria distilled from the P3 + trust ladder (guarded by default; exact facts only; unguarded only + behind P3.6-style structural fences with the differential + harness's shapes lane as hard precondition), joint header layout + with gc-plan P1, and the P4.1–P4.6 implementation checklist with + gates — that checklist lives in shapes-plan.md, which owns the + phase from here. diff --git a/docs/plans.md b/docs/plans.md new file mode 100644 index 00000000..eb127208 --- /dev/null +++ b/docs/plans.md @@ -0,0 +1,187 @@ +# echojs — the program of work + +The single ordering document. Milestones are `P#`, their phases +`P#.#`; each phase *references* a bucket plan's phase (`gc-P2`, +`shapes-P4`, ...) where the design, gates, and results live. Bucket +plans: `compiler-plan.md`, `maam-plan.md`, `shapes-plan.md`, +`gc-plan.md`, `sinking-plan.md`, `runtime-plan.md`, `language-plan.md`, +`release-plan.md`. Results docs (`*-results.md`) record gate numbers +per landed phase. + +Conventions: a milestone is done when every phase is; phases within a +milestone are ordered; milestones are ordered but adjacent future +milestones can interleave when their buckets don't touch. Bucket +phase ids are stable — commit messages and results docs written before +2026-07-25 use the pre-rename ids (maam's bare P0..P4, shapes' P4.1.. +P4.6, sinking's S1..S3, gc's bare P0..P7); each bucket doc carries the +mapping. + +## P1 — The EIR pipeline [x] + +One SSA middle-end, no legacy path. Detail: compiler-plan.md +(history section) and `EIRProposal.md`. + +- [x] **P1.1** close the per-function lowering gaps (424/424, zero + fallbacks). +- [x] **P1.2** desugars run pre-EIR (classes, destructuring, + generators, spread, hoisting). +- [x] **P1.3** toplevel-as-EIR: whole modules lower as one unit. +- [x] **P1.4** flip the default, delete the legacy middle-end (~9k + lines); stage2/stage3 byte-identity under EIR self-compiles. + +## P2 — Typed arithmetic: the maam oracle [x] + +An abstract-interpretation type oracle feeding guarded unboxed +arithmetic. Detail: maam-plan.md; numbers in maam-p0-results.md. + +- [x] **P2.1** oracle adapter + dialect shims (maam-P0). +- [x] **P2.2** ⊤-degradation + node-identity queries (maam-P1). +- [x] **P2.3** low-tier ops: has_tag/unbox/box/f64_* (maam-P2). +- [x] **P2.4** guarded arithmetic diamonds (maam-P3). +- [x] **P2.5** trust-free guard folding, region merging, raw f64 + joins (maam-P3.4). +- [x] **P2.6** differential harness: concreteEval vs node vs ejs + (maam-P3.5). +- [x] **P2.7** typed calling convention / function specialization + (maam-P3.6). ~46× on the phase bench. + +## P3 — Shapes [x] + +Type-aware hidden classes, slot storage, guarded property fast paths. +Detail: shapes-plan.md (designed as maam-P4). + +- [x] **P3.1** runtime shape tracking, dual bookkeeping (shapes-P1). +- [x] **P3.2** slot storage + dictionary migration (shapes-P2). +- [x] **P3.3** shape-guarded fast paths under --types (shapes-P3). +- [x] **P3.4** born with their shape (shapes-P4). +- [x] **P3.5** typed slots + shape/numeric region fusion (shapes-P5). +- [x] **P3.6** measured extensions: 2-way polymorphic guard chains; + accessor inlining/pretenuring/array-shapes declined on evidence + (shapes-P6). + +## P4 — Mover foundations [x] + +The generational moving collector, through precise JS roots. Detail: +gc-plan.md; numbers in gc-p0/p2/p3-results.md. + +- [x] **P4.1** measurement + generator-scan fixes + runtime -O2 + (gc-P0). Verdict that shaped this milestone: pins are tiny, so + the nursery ships on conservative roots. +- [x] **P4.2** 64-bit header + forwarding plumbing (gc-P1). +- [x] **P4.3** nursery + object-remembering barrier + evacuating + minor + emitted inline env allocation, default ON (gc-P2). +- [x] **P4.4** emitter gc-frames: precise relocatable JS roots + env + slot-address inlining + move-everything stress (gc-P3). + +## P5 — Allocation elimination [x] + +Delete the allocations the mover made cheap. Detail: sinking-plan.md. + +- [x] **P5.1** shaped-literal sinking + own-key folding + (sinking-P1). +- [x] **P5.2** epoch-guarded constructor-result sinking — the + types-bench2 alloc loop (sinking-P2). +- [x] **P5.3** flow-sensitive field writes, partial escapes, + rest_args/args_obj (sinking-P3). +- [x] **P5.4** optimizer residue: SSA cleanups, type lattice, + slot-load CSE for toplevel receivers (compiler-P1). DONE + 2026-07-28 — docs/compiler-p1-results.md. + +## P6 — Compacting, shape-fused GC + +The heap shrinks; the collector consumes the object model. Detail: +gc-plan.md, shapes-plan.md (Step B). + +- [x] **P6.1** mostly-copying major compaction + auto-tuned growth + target (gc-P4). DONE 2026-07-26 — docs/gc-p4-results.md. +- [x] **P6.2** shapes intersection: per-shape trace bitmaps, inline + slots, object-literal inline allocation, typed-slot barrier + elision (gc-P5; consumes shapes-plan's deferred Step B). DONE + 2026-07-28 — docs/gc-p5-results.md. +- [x] **P6.3** collector structural refactor: cell-lifecycle module, + LOS lookup, file split (runtime-P4; can land any time after + P6.1, behavior-preserving). DONE 2026-07-29 — + docs/runtime-p4-results.md. + +## P7 — Robustness + +The correctness and ergonomics debts, paid down. Detail: +runtime-plan.md, compiler-plan.md. + +- [x] **P7.1** pinned runtime-bug burn-down (runtime-P1). DONE + 2026-07-29 — docs/runtime-p1-results.md. +- [x] **P7.2** export-boundary wrapper: specialization across escaping + entry points (runtime-P2). DONE 2026-07-29 — + docs/runtime-p2-results.md (wrapper dispatches to an UNTRUSTED + guarded clone — maam's constant-domain claims can't cross the + boundary — plus the escape-taint fence, closing a pre-existing + cross-module trusted-rewrite miscompile). +- [x] **P7.3** value-based test harness, un-pinning node's inspect + format (runtime-P3). DONE 2026-07-29 — + docs/runtime-p3-results.md (harness-owned serializer on both + sides; baselines byte-identical from node 22.4.0 and 22.23.2, CI + floats on 22.x; the un-masking flushed 3 runtime bugs fixed + + 3 pinned, plus a tester scheduler bug that had silently skipped + weakmap2 forever). +- [x] **P7.4** finish the TypeScript port of the compiler; babel step + becomes tsc (compiler-P2). DONE 2026-07-29 — + docs/compiler-p2-results.md (//lib:generated converts modules + with one tsc --allowJs pass; tester.ts and gen-atoms.ts ported; + `generator: esm` baselines byte-identical vs babel-node; babel + removed from the repo). +- [x] **P7.5** clang-style pass configuration: -O suites define the + optimizer tiers, -f/-fno- per-pass flags replace the EJS_* env + vars, which revert to debugging-only (compiler-P5; independent, + can land any time). DONE 2026-07-29 — + docs/compiler-p5-results.md (pass registry in lib/pass-config.ts; + -O2 byte-identical to the pre-P5 default; env spellings deleted + after a 35-pair env≡flag A/B; EJS_FLAGS is the one env escape). + +## P8 — Language modernization + +Catch up with the language; adopt test262. Detail: language-plan.md. + +- [ ] **P8.1** gap inventory + test262 subset probe (language-P1). +- [ ] **P8.2** parser replacement behind the ESTree seam + (language-P2; coordinates with compiler-P3 if TS input + happens). +- [ ] **P8.3** features in payoff order (language-P3). +- [ ] **P8.4** test262 CI lane (language-P4). +- [ ] **P8.5** un-fork the JS external-deps (language-P5). + +## P9 — Distribution + +From repo to product. Detail: release-plan.md, compiler-plan.md. + +- [x] **P9.1** relocatable dist artifact + LLVM toolchain policy + (release-P1). DONE 2026-07-30 — docs/release-p1-results.md + (`//:dist` tarball of the installed layout, `//:test-dist` smoke + test, CI uploads per-platform artifacts; the driver discovers a + matching-major opt/llc and fails loudly otherwise, LLVM_MAJOR + baked into host-config). +- [x] **P9.2** platform packages: homebrew, linux, npm wrapper + (release-P2). DONE 2026-07-30 — docs/release-p2-results.md + (packaging/: prefix installer shipped in the tarball + + `//:test-dist` step, homebrew formula generator + libexec/exec- + shim layout, npm wrapper with EJS_NPM_TARBALL override; CI + smokes all three; hosted URLs await release-P3). +- [x] **P9.3** versioning + release automation off the bootstrap + matrix (release-P3). DONE 2026-07-30 — + docs/release-p3-results.md (CHANGELOG discipline + + prepare-release.sh stamping/tagging; ci matrix refactored into + reusable bootstrap.yml; release.yml on v-tags: version-check → + same matrix → draft release with tarballs/formula/npm tgz → + clean-machine container+runner smokes; npm wrapper is + @pirouette/echojs, the bare name was taken). +- [ ] **P9.4** getting-started surface (release-P4). +- [ ] **P9.5** reusable native modules + IR-in-manifest cross-module + linking (compiler-P4). + +## P10 — Concurrent GC + +Pause bounds independent of live-set size. Detail: gc-plan.md. + +- [ ] **P10.1** collector thread: concurrent mark (SATB) + STW + survivor evacuation (gc-P6). +- [ ] **P10.2** fully concurrent evacuation — only on P10.1's pause + evidence (gc-P7). diff --git a/docs/release-p1-results.md b/docs/release-p1-results.md new file mode 100644 index 00000000..10c86f84 --- /dev/null +++ b/docs/release-p1-results.md @@ -0,0 +1,104 @@ +# release-P1 results: relocatable dist artifact + LLVM toolchain policy + +Status: DONE 2026-07-30 (P9.1 in plans.md). + +## What an installed echojs IS + +The driver always had a latent installed layout (every non-`--srcdir` +path in ejs-es6.ts); release-P1 makes it a real, shippable artifact: + + echojs--/ + bin/ejs the self-hosted compiler (stage2) + include/*.h runtime headers (-I at final link) + lib//libecho.a merged runtime archive + lib//libpcre16.a + lib//libdouble-conversion.a + lib/node-compat.ejs native-module manifest (lib/ is + lib//libejsnodecompat-module.a the moduledir) + LICENSE.txt, README.md + +`buck2 build //:dist` produces the tarball (in an out *directory*, +since the version isn't knowable at analysis time; version comes from +package.json until release-P3 owns versioning). `buck-dist.sh` repacks +`//:srcdir-tree` — the same bits the bootstrap matrix proves — plus the +stage2 executable, which the stage3 fixed point vouches for. + +Deliberately excluded: the ejs-llvm module. Its manifest bakes the +build machine's `llvm-config --ldflags --libs`, and only the bootstrap +imports `@llvm`; reusable native modules are compiler-P4 (P9.5). + +## LLVM toolchain policy: discover, verify, fail loudly + +The dist can't vendor `opt`/`llc` (~100MB+ per platform) and can't +trust the baked build-machine bindir (on a user's machine that path may +hold a different major — and the llvm@16-on-PATH incident showed a +mismatched `opt` miscompiles *silently*: llvm-22 module-init stores +became `unreachable` traps with exit code 0). So: + +- `//lib:host-config.js` now bakes `LLVM_MAJOR` (from `llvm-config + --version` at build time) alongside `LLVM_BINDIR`. +- The driver resolves the tool bindir lazily (first compile, so + `--help` never probes), in order: + 1. `LLVM_BINDIR` env — explicit override, `""` = plain PATH; still + version-checked, `EJS_LLVM_NO_VERSION_CHECK=1` forces past it; + 2. the baked build-machine bindir; + 3. conventional locations (`/opt/homebrew/opt/llvm{@N,}/bin`, + `/usr/local/opt/llvm{@N,}/bin` on macos; `/usr/lib/llvm-N/bin` on + linux), then bare PATH. +- Every candidate is verified by parsing `LLVM version (\d+)` out of + `opt --version`; the first matching-major candidate wins; if none + match the driver exits with an actionable message (what it tried, + what each had, how to install/point at LLVM N). +- The probe captures output via `sh -c '... > tmpfile'` + + `readFileSync` — the one capture mechanism the node-hosted and + self-hosted drivers share (self-hosted `spawn` returns only the exit + status). +- The never-spawned `llvm-as` entry in the tool table is gone. + +Verified by hand: baked-path success; `LLVM_BINDIR=/nonexistent` fails +loudly (exit 255, names the required major); `LLVM_BINDIR=""` with no +opt on PATH fails loudly; with llvm on PATH succeeds; the +no-version-check escape reaches the tool spawn and fails there via +spawnSyncChecked (pre-existing behavior). + +## Smoke test + +`buck2 build //:test-dist` unpacks the tarball into scratch and, with +the build LLVM *off* PATH (discovery must work as a user's machine +would): + +1. compiles + runs a no-import program (classes, template strings, + arrow lambdas) — output checked; +2. compiles + runs a `@node-compat/path` import — exercises the lib/ + manifest scan and the `lib//` module archive; +3. asserts the fail-loudly path: `LLVM_BINDIR=/nonexistent` must exit + nonzero with the version-policy message and produce no executable. + +## CI + +Both jobs build `//:test-dist` then `//:dist --out` (the stage builds +are shared, so this adds only the repack + smoke compile) and upload +the tarball: `echojs-dist-macos-arm64`, `echojs-dist-linux-{arm64,x86_64}`. + +## Gates + +- tsc typechecks clean (tsconfig.json + test --noEmit) +- `//:test-eir` green +- full matrix `//:test-stage{0,1,2,3}` + shapes-off + lowtier green +- `//:dist` + `//:test-dist` green + +## Follow-ons + +- ~~The runtime's EXCEPTIONS spew (ejs-exception) is noisy on stderr + during every native-module import resolution.~~ FIXED same day: + `#define spew 1` had been hardcoded on in ejs-exception.c since + forever; now 0 (the ejs-gc-internal.h convention — flip the define + to trace exception dispatch). Full matrix re-run green. +- `bin/ejs` ships unstripped (~debug-sized); strip at dist time once a + symbol-preservation story exists. +- Linux compiled programs need libuv/libunwind dev packages at link + time; the README documents it, release-P2 packaging should depend on + them properly. +- macOS ld warns that dist archives (built for the SDK, 15.7) are newer + than the default `-mmacosx-version-min` (osx_min 11.0 → linked 15.0 + objects); pre-existing in srcdir mode too, harmless but noisy. diff --git a/docs/release-p2-results.md b/docs/release-p2-results.md new file mode 100644 index 00000000..63e9bcdc --- /dev/null +++ b/docs/release-p2-results.md @@ -0,0 +1,91 @@ +# release-P2 results: platform packages + +Status: DONE 2026-07-30 (P9.2 in plans.md). Everything layers on the +release-P1 dist tarball; nothing here touches the compiler or runtime. + +## The one structural fact all three packages obey + +The driver resolves `include/` and `lib/` relative to its own binary +via `argv[0]` and **does not chase symlinks** (ejs_exe_dirname in +ejs-es6.ts). So no package may put a symlink on PATH pointing into the +layout; each uses an absolute-path exec shim instead, and the layout +stays whole in one directory. + +## Tarball additions (buck-dist.sh) + +- `dist-info` at the tarball root: sh-sourceable metadata + (`EJS_VERSION`, `EJS_TRIPLE`, `EJS_SHORT_TRIPLE`, `EJS_OS`, + `EJS_LLVM_MAJOR`). Every packaging layer reads it from the artifact + instead of re-deriving facts about the build. +- `install.sh` (from `packaging/install.sh`) ships at the root. + +## The packages + +- **Prefix installer** (`packaging/install.sh`, in every tarball): + `./install.sh [--prefix /usr/local]` copies the tree to + `$PREFIX/lib/echojs/` and writes the `$PREFIX/bin/ejs` exec + shim; `--uninstall` reverses it (and only removes a shim that points + into its own tree). Best-effort post-install advice: probes the + driver's conventional LLVM locations for a matching major, and on + linux checks `ldconfig -p` for the libuv/libunwind **dev** symlinks + (`libuv.so ` with the trailing space — `.so.1` alone is just the + runtime lib). Warnings only; the driver stays the authority and + fails loudly. +- **Homebrew** (`packaging/homebrew/`): `echojs.rb.in` + + `make-formula.sh --tarball … [--url …] [--out …]` which fills url, + sha256, version, and llvm major from the tarball's dist-info + (refuses non-macos tarballs). The formula installs the whole layout + under `libexec` and `bin.write_exec_script`s the shim — a brew link + farm is exactly the symlink shape the driver can't follow. (Learned + the hard way: `(bin/"ejs").write_exec_script …` creates a *directory* + `bin/ejs/`; the receiver is the dir, the argument the target.) + `depends_on "llvm@N"`: homebrew-core keeps a versioned alias for the + current major (llvm@22 → llvm 22.1.8 today) and a real versioned + formula after it's superseded, so the pin survives brew's llvm + moving on. Homebrew rejects loose formula files now — install goes + through a tap (`brew tap-new`; release-P3 should push generated + formulas to a real `toshok/homebrew-echojs`). +- **npm wrapper** (`packaging/npm/`, name `echojs`): postinstall + downloads `echojs--.tar.gz` from the + `v` GitHub release (darwin-arm64, linux-arm64, linux-x64 → + short triples) and unpacks it as `dist/`; `bin/ejs.js` spawnSync's + `dist/bin/ejs` (node realpaths the main module, so the `.bin` + symlink is harmless — `__dirname` is the package's true location). + `EJS_NPM_TARBALL=/path/to/tarball` overrides the download: the CI + path, the offline escape, and the only way to test before release-P3 + hosts assets. Wrapper version == dist version == release tag; the + publish flow is release-P3's. + +## Verification + +- `//:test-dist` grew step 4: install into a scratch prefix, compile + through the shim, uninstall, assert nothing is left. Runs on all + three CI platforms. +- CI macos job: builds the formula from the just-built tarball via a + throwaway `--no-git` tap, `brew install` + shim compile + `brew + test` + uninstall; then the npm wrapper via `npm pack` + install + with `EJS_NPM_TARBALL` + shim compile. Linux jobs: the npm smoke. +- Locally verified on macos arm64: full brew tap/install/compile/ + `brew test`/uninstall cycle green; npm pack/install/compile green; + `//:dist` + `//:test-dist` green. + +## Removed + +2016-era bitrot from the make/llvm-3.4 build: `debian/`, +`release/` (trusty64 vagrant), `packaging/npm/package.json.in`, +`packaging/.gitignore`. + +## Follow-ons + +- release-P3 owns: hosted release assets (which make the formula's + `--url` mode and the npm download path real), a + `toshok/homebrew-echojs` tap, npm publish, version stamping (root + package.json is still 0.0.0), and deb/rpm if tarball+install.sh + proves insufficient. +- ~~The npm package name `echojs` may be taken on the registry — + check at first publish (scoped fallback).~~ + RESOLVED in release-P3: it was taken (an unrelated 0.1.4); the + wrapper is scoped — `@pirouette/echojs` (the @pirouette npm org). +- macos ld's version-min warnings (release-P1 follow-on) now also + surface through every package's compile smoke; still harmless, + still noisy. diff --git a/docs/release-p3-results.md b/docs/release-p3-results.md new file mode 100644 index 00000000..bf0c3cbf --- /dev/null +++ b/docs/release-p3-results.md @@ -0,0 +1,123 @@ +# release-P3 results: versioning + release automation + +Status: DONE 2026-07-30 (P9.3 in plans.md). The machinery is in place +and verified as far as it can be without pushing a tag; the first real +release exercises the pipeline end to end. + +## The scheme + +- Semver, pre-1.0 reading (0.MINOR may break, PATCH may not), + documented in CHANGELOG.md's header. The first pipeline release is + **0.2.0**: the 2016-era make-build era already shipped tags up to + `0.1.0` (unprefixed — the pipeline's `v`-prefixed tags can't + collide, but the version numbers shouldn't be reused). The tree + carries the to-be-released version between releases (0.2.0 now); + prepare-release stamps with `--allow-same-version` so that's fine. +- The version lives in exactly two files — `package.json` (what + buck-dist.sh stamps into the tarball/dist-info) and + `packaging/npm/package.json` (what pins the wrapper's download tag) + — plus the git tag. Nothing else carries a version; the release + pipeline refuses a tag where the three disagree. +- CHANGELOG.md is Keep-a-Changelog-shaped, newest first, with an + Unreleased section that must be non-empty to cut a release (an empty + entry means the release story wasn't written). Seeded with the + first-release Unreleased content. + +## Cutting a release + +`./packaging/prepare-release.sh 0.2.0` (local, offline): clean-tree +check, rolls Unreleased into `## [0.2.0] - `, stamps both +package.jsons via `npm version --no-git-tag-version` (which also +updates the lockfile's mirrored version), commits `release: v0.2.0`, +makes the annotated tag. It deliberately does NOT push — pushing the +tag is the human act that starts the pipeline. + +## The pipeline (.github/workflows/release.yml, on v tags) + +1. **version-check** — tag == both package.jsons, CHANGELOG section + exists. +2. **bootstrap** — ci.yml's jobs were refactored into a reusable + `bootstrap.yml` (`on: workflow_call`); CI and Release both call it, + so "a release is a green matrix" is literally the same workflow: + stage ladder + test-eir, dist tarballs + //:test-dist (which + includes the installer smoke), and the release-P2 package smokes on + all three platforms. +3. **publish** — downloads the three tarball artifacts, generates the + homebrew formula against the hosted asset URL (sha from the real + tarball), `npm pack`s the wrapper, extracts the tag's CHANGELOG + section as notes, and creates a **draft** GitHub release carrying + tarballs + formula + wrapper tgz. Publishing the draft is the + go-live act — draft asset URLs aren't public, so the formula and + the npm postinstall only resolve after that click. Two + shell-gated legs, each loudly skipped when unconfigured rather + than breaking the release: the formula push to + `toshok/homebrew-echojs` (iff `HOMEBREW_TAP_TOKEN` is set — a git + push needs a credential), and `npm publish` via **OIDC trusted + publishing** (docs.npmjs.com/trusted-publishers): no token at all — + the job has `id-token: write`, npm ≥ 11.5.1 exchanges the GitHub + OIDC token for short-lived credentials, and provenance + attestations are generated automatically. Gated on the + `NPM_TRUSTED_PUBLISHING` repo *variable* being `true`, flipped + after the trusted publisher is configured on npmjs.com. Two + load-bearing details: the publisher config matches owner/repo + + the workflow *filename* (`release.yml` — renaming the file breaks + publishing; the publish step must also live in this workflow, not + a reusable one, since validation checks the calling workflow), and + the wrapper's `repository` field must match the repo exactly + (`git+https://github.com/toshok/echojs.git` + `directory: + packaging/npm`). The publish uses the package directory, not the + tgz, so provenance sees the build context; `publishConfig.access: + public` is baked into the wrapper's package.json. +4. **smoke-linux / smoke-macos** — the clean-machine proof the plan + asked for: a bare `ubuntu:24.04` container (both arches) and a + fresh macos runner that never see the repo install only the + tarball plus the README's documented prerequisites (apt.llvm.org + llvm-22 + build-essential + libuv/libunwind dev on linux; `brew + install llvm` on macos), run `install.sh`, and compile + run a + program through the installed layout. + +## npm name + +`echojs` is taken on the registry (an unrelated 0.1.4), so the wrapper +is `@pirouette/echojs` under the @pirouette npm org (bin is still +`ejs`); the release-P2 follow-on is resolved. CI smoke globs updated +for the scoped pack filename (`pirouette-echojs-*.tgz`). A relative +`EJS_NPM_TARBALL` resolves against `INIT_CWD` (where `npm install` was +invoked), since postinstall's cwd is the package directory. + +## Verified locally + +- prepare-release.sh dry-run in a scratch clone: stamps all four + files, rolls the changelog, tags v0.1.0; a second cut correctly + refuses on the now-empty Unreleased section. +- All three workflows parse and pass `actionlint` (only intentional + SC2016 infos remain: single-quoted JS template literals). +- make-formula.sh `--url` mode produces the hosted-URL formula with + the local tarball's sha256. + +## First-release checklist (for whoever pushes the button) + +1. `./packaging/prepare-release.sh 0.2.0` && `git push origin HEAD v0.2.0` +2. wait for the Release workflow: green matrix + draft release + smokes +3. publish the draft release (this makes formula/npm URLs real) +4. optional, once: create `toshok/homebrew-echojs` and set + `HOMEBREW_TAP_TOKEN` — until then the formula rides on the release + page +5. optional, once, for npm: on npmjs.com, add a trusted publisher to + `@pirouette/echojs` (org `toshok`, repo `echojs`, workflow + `release.yml`, allowed action `npm publish`), then set the repo + variable `NPM_TRUSTED_PUBLISHING=true`. If npmjs won't accept a + trusted publisher for a never-published package, do the first + `npm publish --access public` locally as an @pirouette member, + then configure it — every later release publishes via OIDC + +## Follow-ons + +- The publish job re-runs are not idempotent (`gh release create` + fails if the draft already exists) — delete the draft before + re-running, or teach the step `gh release view || create`. +- The linux smoke pins apt.llvm.org's llvm-22 spelling; when the + toolchain major moves, dist-info already carries it — the smoke + could read EJS_LLVM_MAJOR from the tarball instead of hardcoding. +- P9.4 (getting-started surface) should point the README at the + released packages instead of the repo build. diff --git a/docs/release-plan.md b/docs/release-plan.md new file mode 100644 index 00000000..c26dfe23 --- /dev/null +++ b/docs/release-plan.md @@ -0,0 +1,48 @@ +# release-plan: packaging and distribution + +Bucket plan; the ordering spine lives in `docs/plans.md` (milestone +references look like `release-P1`). New bucket (2026-07-25): the +buck2 build is great for cross-platform development, but it is not the +answer for people who just want to download a package and go. + +## Phases + +- [x] **release-P1 — Relocatable binary artifact.** DONE 2026-07-30 — + docs/release-p1-results.md. Define what an + installed echojs IS: the `ejs` driver binary, the runtime static + libraries (`libecho.a` + friends), the srcdir headers/manifests + the driver needs, and a pinned LLVM toolchain policy (today the + driver spawns `opt`/`llc` from a baked bindir — an installed + package must either vendor the LLVM tools it needs or discover a + compatible installation and fail loudly; the llvm@16-on-PATH + miscompile taught us "fail loudly"). Deliverable: a `buck2 + build //:dist` (or script) that produces a self-contained, + relocatable tarball per platform, exercised in CI. +- [x] **release-P2 — Platform packages.** DONE 2026-07-30 — + docs/release-p2-results.md. Homebrew formula/cask for + macOS (arm64 first), a deb/rpm or tarball+install.sh for Linux + (arm64 + x86_64 — the CI bootstrap matrix already proves the + targets). An npm wrapper package is worth considering for the + node-adjacent audience (postinstall fetches the platform + tarball). (Shipped: tarball+install.sh — deb/rpm deferred + unless it proves insufficient — plus the formula generator and + the npm wrapper; every package uses an absolute-path exec shim + because the driver doesn't chase symlinks. Hosted asset URLs, + the real tap, and npm publish are release-P3's.) +- [x] **release-P3 — Versioning + release automation.** DONE + 2026-07-30 — docs/release-p3-results.md. Semver + scheme, a changelog discipline, tagged releases built by CI from + the bootstrap matrix (a release is a green matrix + packaged + artifacts + smoke test of the installed package compiling a + hello-world on a clean machine/container). (Shipped: + CHANGELOG.md + prepare-release.sh + reusable bootstrap.yml + + release.yml drafting the release and running bare-container/ + fresh-runner install smokes; the tap push is gated on its + token, npm publishes via OIDC trusted publishing — no npm + token exists anywhere. The first pushed tag is the end-to-end + proof.) +- [ ] **release-P4 — Getting-started surface.** A quickstart README + path that assumes the package (not the repo): install, compile a + file, link a multi-module program; document the supported + language subset honestly (pointing at language-plan status) + and the flag surface (`--types`, GC knobs) that users may touch. diff --git a/docs/runtime-p1-results.md b/docs/runtime-p1-results.md new file mode 100644 index 00000000..3735a1ed --- /dev/null +++ b/docs/runtime-p1-results.md @@ -0,0 +1,150 @@ +# runtime-P1 results — pinned-bug burn-down (plans P7.1) + +Phase record for runtime-plan.md's runtime-P1: the ten pinned runtime +bugs, fixed. Landed 2026-07-29 on `eir`. Every fix was verified +against node 22.4.0 on a direct repro before the suite gates ran. + +## The ten, and what each turned out to be + +1. **`typeof null` → `"object"`.** Three coupled sites: the runtime + mapping (`_ejs_op_typeof`), the compiler's constant fold + (cleanup.ts `TYPEOF_OF_TAG`), and the `typeof x === "T"` peephole's + runtime helpers. `_ejs_op_typeof_is_object` now admits null — and + excludes functions, which it had wrongly admitted (`typeof + function(){} === "object"` was `true`; the helper never matched + `_ejs_op_typeof`'s function-first ordering). `typeof_is_null` is + constant false. typeof1.js un-pinned (`generator: none` dropped — + node now agrees). + +2. **`-0 === 0`.** The strict_eq NaN-box TAG compare ran before the + numeric compare, and ±0 are different bit patterns. Numbers now + compare first, by IEEE `==` (NaN and ±0 exact per spec). The same + tag-first flaw was latent in `_ejs_op_eq` (step 3's "same Type"), + `SameValue` (whose step 6c had a typo making `Object.is(0,0)` + FALSE), and `SameValueZero` (whose guard let a string/object pair + fall into the string compare) — all four fixed. cleanup.ts's + equality-fold decline over `-0` consts (compiler-P1's workaround + for the runtime quirk) is deleted; math2.js un-xfailed. + +3. **`Math.round(-2.5)` → `-2`.** C `round()` ties away from zero; + ES ties toward +∞. Now `floor(x + 0.5)` with the two exactness + screens (|x| ≥ 2^52 already integral; |x| < 0.5 returns ±0 — the + `0.49999999999999994` case where `x + 0.5` rounds to 1.0). + +4. **`Number(" 7 ")` → `7`.** ToNumber's string path is a real + StringToNumber now: trims the ES WhiteSpace ∪ LineTerminator set + (on the UCS-2 code units), empty → 0, exactly-"Infinity" (strtod's + "inf"/"nan" spellings rejected), 0x hex (digits only, no sign, no + hex-float exponent), ES6 0b/0o, NaN on any non-ASCII unit. + +5. **`-8 >>> 28` → `15`.** The shift family cast the double operand + straight to unsigned (UB; arm64 saturates negatives to 0). All + four shifts now go through ToInt32/ToUint32 — which also un-aborts + their string/object operand paths. `ToUint32` itself did the same + UB cast and is now `(uint32_t)ToInt32`; bitand/bitor/bitnot moved + from int64-truncating ToInteger to ToInt32. + +6. **`1 + null` aborted.** ToNumber had no null case (→ 0 now). Add + also tested the *original* operands for stringness rather than the + ToPrimitive results, so `({}) + 1` numeric-added to NaN instead of + concatenating — the ES string test is on lprim/rprim; fixed. + +7. **`"a" * "b"` aborted.** mult/div/mod were number-lhs-only with + NOT_IMPLEMENTED arms; each is now just ToNumber both sides (with + explicit evaluation order — sub too, whose C argument order was + unsequenced). + +8. **Uncaught throw out of a generator body.** The desugar's outer + catch rethrows on the generator's makecontext stack, and the + unwinder walked off it into terminate. `_ejs_generator_start` now + invokes the body through `_ejs_invoke_closure_catch` (the runtime's + existing landing-pad wrapper): the exception parks in the + generator (`threw_out`), the context swaps back normally, and + every resume site rethrows via `_ejs_generator_resume_result` — on + the CALLER's stack. `.next()` after the throw keeps answering + `{ undefined, true }` per 25.3.3.3. The four generator xfails + (5/6/15/16) are a different debt (yield-expression sent values) + and stay pinned. + +9. **Sparse-array set NOT_IMPLEMENTED.** The Arraylet type existed + but nothing read or wrote one. Implemented: fixed 512-slot + chunk-aligned arraylets, sorted by start_idx (binary search; + aligned chunks can't overlap), created on demand full of the same + hole magic dense arrays use. Get / GetOwnProperty / HasProperty / + Set / Delete / DefineOwnProperty and length-shrink truncation all + handle the sparse case; storage iteration (not length iteration) + keeps `new Array(1e9)` O(present-elements). sparsearray1.js + un-xfailed. + +10. **getOwnPropertyNames.** Three defects: it filtered out + non-enumerables (the pinned divergence — the filter belongs to + Object.keys, not here), it threw-NOT_IMPLEMENTED on primitives + (ES6 ToObject-coerces; null/undefined still TypeError), and it + only walked the property map, so array / String-object index + properties and `length` never appeared. Index names now come + first (OrdinaryOwnPropertyKeys order) via + `_ejs_array_push_own_index_names` (arraylet-aware) or the + String's primStr length, then `length`, then the map walk. + +## Adjacent fixes the burn-down surfaced + +- **C-side exception catchers leaked the gc-frame chain** + (ejs-function.c / ejs-invoke-closure-catch.ll). Emitted CATCH + handlers re-link their own frame record as the chain head after an + unwind; `_ejs_invoke_closure_catch` / `_ejs_invoke_func_catch` — C + catchers with no frame record — left `_ejs_heap.gc_frame_head` + pointing at the unwound (dead) emitted frames. The generator fix + made this reachable deterministically: the body's exception is + caught on the generator stack, `pop_generator` parked the stale + head, and the next minor walked dead frame records (segfault under + EJS_GC_EVERY_N_ALLOC=7, no VERIFY needed; found by the stress sweep + over the new repros). Fix: the .ll wrappers became `*_inner` and C + wrappers restore the saved chain head on the catch path — which + also closes the same latent hazard at every existing C catcher + (promise reactions, Map/Array.from ingestion, the iterator + helpers). Same lesson as runtime-P4's two finds: anything that + depends on C-stack luck is a latent bug. +- **console.log formatting** (differential-lane fidelity, both + pre-existing): `-0` prints as `-0` (node's inspect distinguishes + it; ToString still collapses to "0" per spec), and strings nested + in arrays print quoted (`[ 'a' ]`). proxy6.js — pinned with + `generator: none` precisely because of the unquoted format — is + node-generated again. + +## Pre-existing issues observed, NOT fixed here + +- The PARANOID stress lane's generator failures (gc-gen*/generator* + × EJS_GC_PARANOID) reproduce bit-for-bit on the phase-entry + runtime — the recorded baseline set, unchanged. +- Under lldb's address layout, EJS_GC_EVERY_N_ALLOC=7 crashes during + `_ejs_init` (xhr init setprop reads a 0xa7-poisoned cell) on the + phase-entry runtime too — an environment-sensitive + conservative-scan-luck use-after-free during init, recorded for a + future stress pass. + +## Un-pinned tests + +typeof1 (generator:none dropped), math2 (xfail dropped), sparsearray1 +(xfail dropped), proxy6 (generator:none dropped). Still pinned, with +reasons unchanged: generator5/6/15/16 (yield sent values), math1 (ES6 +Math functions), forin2/5, object6/7/9, and the rest of the xfail set +— none of them runtime-P1 items. + +## Gates + +- Matrix: test-eir-lowtier + stage0–3 (including the stage2/stage3 + byte-identity fixed point) + stage1-shapes-off — all green. + Stage suites now 424 pass / 20 xfail / 0 fail (math2 and + sparsearray1 un-xfailed and passing). +- test-eir: exactly the 11 pre-existing compiler-P1.1 failures + (born-shaped test debt recorded at runtime-P4 close), nothing new. +- Stress sweep over every new-path repro (uncaught generator throw, + sparse arrays, getOwnPropertyNames, the full value-op battery): + EVERY_N_ALLOC 7/31/101 × VERIFY/PARANOID + NURSERY=off + + COMPACT=off, all green. The gc/generator test stress lane matches + the phase-entry baseline failure set exactly (PARANOID generator + items only, verified pre-existing by A/B against the phase-entry + libecho). +- Direct repro battery (every bug above, constant and non-constant + operand forms, plus Object.is/edge cases): byte-identical to node + 22.4.0 output. diff --git a/docs/runtime-p2-results.md b/docs/runtime-p2-results.md new file mode 100644 index 00000000..bf75f9fa --- /dev/null +++ b/docs/runtime-p2-results.md @@ -0,0 +1,146 @@ +# runtime-P2 results — the export-boundary wrapper + the escape-taint fence + +Phase P7.2 (docs/plans.md), bucket runtime-plan.md. Landed 2026-07-29 +on branch `eir`. The standing follow-on recorded by maam-plan (P3.6: +"the boundary-guard wrapper dispatching escaping entries to the clone +remains OPEN follow-on work") and sinking-plan, paid down — with one +deliberate design change from the sketch, and one pre-existing +soundness bug found and fixed on the way. + +## Why the wrapper does NOT dispatch to the trusted clone + +The plan sketch said "generic signature outside, dispatching to +specialized/trusting internals." That is unsound, and the reason is +maam's value domain: **constant propagation** (numbers and strings are +tracked as constants up to a widening bound — src/lang/values.ts in +echojs-maam). A claim maam makes about an escaping function's body can +be conditioned on the argument *constants* its analyzed call sites +passed — e.g. it prunes a `y > 5` branch entirely under `y = 3` — so +the claim can be false for an external call passing 7: a NUMBER. A +boundary tag guard proves tags, not maam's entry state, so no has_tag +chain can license entering a trusted (unguarded, SpecMode) clone from +an un-analyzed caller. + +What the wrapper dispatches to instead is an **untrusted clone** +(SpecMode.trusted = false, lower.ts): + +- typed signature: f64 formals, boxed once at entry — `box_f64` is the + optimizer's structural number proof; +- the body keeps the ordinary guarded diamonds; the diamond gate widens + to assume-and-guard (`operandPlausiblyNumber`: only a POSITIVE + non-number claim declines — nodes the oracle never saw, the norm for + an exported-but-never-called-internally function, guard rather than + decline); +- result stays boxed ("any"); no unguarded return unbox. + +The optimizer then folds the formal-rooted diamonds *structurally* — +no oracle claim is ever consumed as fact. The unit test asserts the +strong version: the optimized clone of the standard loop kernel carries +ZERO has_tag guards and raw f64 arithmetic, i.e. trusted-clone quality, +trust-free. (One enabling fix: wrapper compiles run a second +optimizeModule pass after specialization — the loop-carried number +proofs only fit provenNumberAt's depth cap after cleanup's +trivial-param pruning, which runs at a pass's tail. Wrapper-free +compiles skip it, byte-pure.) + +The wrapper itself (specialize.ts installWrapper): a fresh entry block +takes over the calling-convention params; one `has_tag(number)` per +formal chains to a fast block (unbox all, `call_typed` the clone, +return its boxed result); any failure branches to the untouched +original entry — the generic body, full dynamic semantics. Both +external callers (through the module slot) and internal ones (devirt +direct-calls the generic entry; LLVM inlines the prologue) reach the +same guards. Candidacy: escaping closure + the static callee checks +(no rest/arguments/defaults, identifier params, ≥1 formal) + a payoff +check (the lowered clone must emit ≥1 diamond). `EJS_NO_EXPORT_WRAPPER` +bisects. + +## The pre-existing bug: trusted rewrites inside escaping functions + +The same coverage argument turned up a live miscompile that PREDATES +this phase: call sites *hosted inside* an escaping function were being +rewritten to trusted clones. An external caller enters the escaping +function with values the analysis never saw; those values flow to the +hosted site; the rewrite unboxes them unguarded against claims derived +from module-internal constants. types-wrapperfence1 pins the exact +shape: + +```js +function g(y) { var s = y > 5 ? "s" : y; return s * 2; } // private +export function f(x) { return g(x); } +console.log(g(3)); console.log(f(3)); // analyzed +``` + +maam prunes `y > 5` under the analyzed 3, types `s * 2` as num, g +trusted-clones, and the g-site inside f rewrote to `unbox_f64` of an +argument that is `"s"` when main calls `f(7)` — garbage where node +prints NaN. (Verified live before the fix by the probe's stats: +`specialized=1` proves the claim existed.) + +**The fix — the escape-taint fence** (specialize.ts): `tainted` = the +set of Funcs whose activations can observe un-analyzed values — the +escaping closures, closed under (a) callee-of-a-site-hosted-in-tainted +(arguments are tainted) and (b) created-inside-tainted (captured +environment is tainted). Unknown-callee calls need no edge: a value +only becomes callable from tainted code by flowing there, which +already classifies its function as escaping. Rules: + +- an escaping function never gets a trusted clone (it takes the + wrapper path); +- no site hosted in a tainted function is rewritten to a trusted + clone (`specFenced` counts them; a clone with no coverable site is + not minted); +- a tainted-but-non-escaping helper MAY still be trusted-cloned: the + clone is entered only through rewritten sites in covered code, and + every covered activation runs during module init — before any + external caller can exist. Its tainted (generic-entry) activations + run the generic body. + +Residual, documented: an import cycle can re-enter a partially +initialized module, so "covered code runs before external callers" has +that one corner; taint does not model it. The fence has no off-switch +— it is a soundness fix, not an optimization. + +## Gates + +- **eir unit tests**: 213 pass (the 11 standing compiler-P1.1 + born-shaped pins remain, untouched by this phase). New tests: the + wrapper's structure and full guard-fold, decline paths (no payoff / + env capture / frame ops), EJS_NO_EXPORT_WRAPPER, and the sharpened + escaping-closures test (wrapped, never trusted, even under a lying + stub oracle). +- **--types diff lane**: 476 files — 475 identical, 0 divergent, 1 N/A + (tester.js, the standing esprima gap). Includes the new probes. +- **probes** (test/types/README.md census updated): + - types-wrapper1: `specWrapped=1`; numbers cross the boundary into + the clone, a string and a missing arg fail the chain onto the + generic body; flag-off/--types identical. + - types-wrapperfence1: `specialized=1 specSites=1 specFenced=1`; + `f(7)` → NaN, node-identical (the divergence this would have been + is the pinned bug). + - types-specescape1 (existing): now `specWrapped=1`, still identical + and node-matching. + - types-wrongoracle1 (existing): lib's exported `inc` now wrapped; + `inc("x")` still routes generic → "x1". +- **types-bench5** (the headline): the types-bench1 workload with the + kernel EXPORTED and every call crossing the module boundary. + flag-off 0.34 s → **0.07 s user** with the wrapper (~4.9×), exact + PARITY with types-bench1's closed-world trusted path on the same + machine — the export boundary now costs one has_tag per formal per + call. +- **bootstrap matrix**: recorded in the phase-close commit (flag-off + compiles are byte-pure by construction — specialization only runs + under --types, and the second optimizer pass only when a wrapper was + minted). + +## Follow-ons recorded + +- wrappers / guarded per-site dispatch for tainted-called internal + helpers (today they simply stay generic inside tainted hosts); +- a payoff gate that credits call-heavy bodies — a bare delegation + export (`export function f(x) { return g(x); }`) currently declines + its wrapper (`specRejected=1` in types-wrapperfence1); +- maam-side escape hardening (synthetic ⊤-argument entry contexts for + escaping closures) would let the oracle itself account for external + callers — heap flows included — and dissolve the import-cycle + residual. diff --git a/docs/runtime-p3-results.md b/docs/runtime-p3-results.md new file mode 100644 index 00000000..28ed725b --- /dev/null +++ b/docs/runtime-p3-results.md @@ -0,0 +1,141 @@ +# runtime-P3 results: the value-based test harness + +Phase record for runtime-P3 (plans.md P7.3). Baselines no longer encode +node's `util.inspect` format; CI's node pin is gone. + +## What the problem was + +Test baselines (`test/expected/*.expected-out`) were generated live by +running `node ` and comparing raw stdout. Anything a test logged +went through node's inspect formatting on the generation side and +through `console_toString` (a hand-rolled C imitation of some node +version's inspect) on the ejs side. Two consequences: + +- node upgrades broke the suite spuriously (22.4 → 22.23 changed array + formatting), so CI pinned node 22.4.0 exactly; +- worse, the mtime-based regeneration meant most committed baselines + were YEARS stale — generated by ancient node versions and never + refreshed — and tests "passed" only because ejs's C formatter happened + to match that ancient output. Several real semantic divergences were + hiding under that luck (below). + +## What landed + +**`test/harness-console-shim.js`** — a serializer owned by the harness, +conservative ES5, that replaces `console.log/warn/error` with functions +that format VALUES itself and hand the original console.log one final +string. The same file runs in both engines: + +- node side: `harness-run.js` (requires the shim, then the test) is the + generation driver — `node harness-run.js `, or babel-node for + the import-syntax tests; +- ejs side: tester.js compiles a generated two-line wrapper + (`import "./harness-console-shim"; import "./";`) with + `-o .exe`, deleted after each compile. `generator: none` tests + keep the legacy raw-stdout path (no shim, checked-in baseline). + +Baselines now compare equal iff the logged values agree; no engine's +inspect format is in the loop. **Cross-version proof**: all 392 +generable baselines regenerate byte-identically under node 22.4.0 and +node 22.23.2 — the exact release pair whose drift forced the pin. CI +now floats on `node-version: 22.x`. + +The serializer's format is frozen here, not in any engine: node-22.4-ish +for the common cases (`[ 1, 2 ]`, `{ a: 1 }`, quoted nested strings, +`-0`, ``, `[Function: f]`, `Map(n) { k => v }`, +`[Name: message]` for errors, UTC ISO for dates). Two ejs gaps are +deliberately absorbed inside the shim rather than exposed to every +baseline: `Object.keys(array)` omits index keys in ejs, and +`Date.prototype.toISOString` is missing (ISO computed from `getTime()` +with civil-date math). `TZ=UTC` is pinned by tester.js for both +generation and runs so local-time Date construction (date3.js) can't +make a baseline machine-dependent. + +## Harness bugs fixed along the way + +- **tester scheduler skipped a test**: the old `processTests` seeded + `i = test_threads` but incremented `i` before reading `tests[i]` in + the callback — the test at index `test_threads` (here: weakmap2.js) + silently ran in NEITHER the generation nor the run pass, forever + green on a stale baseline. Rewritten; weakmap2 runs and passes. + (The `-t` path's "workaround for a bug" thread-count clamp was this.) +- **concurrent compiles clobbered temps**: every test compile now + includes the shim module, and the compiler's temp names + (`genFreshFileName`) are only unique within one process — 4-way + concurrent compiles raced on `harness-console-shim.js.N-*.bc` (113 + spurious compile failures on the first full run). tester.js gives + each compile its own TMPDIR (the types-diff lane's per-worker + discipline); `os.tmpdir()` honors TMPDIR in both node and the + ejs-compiled compilers. +- **harness staleness**: expected-outs regenerate when the shim or + run driver is newer, not just the test. + +## Runtime bugs the un-masking flushed (fixed) + +- **native error prototypes had null [[Prototype]]** + (`runtime/ejs-error.c`): `Error.prototype` now chains to + `Object.prototype` and the six NativeError prototypes to + `Error.prototype` (ES2015 19.5.6.3) — `e instanceof Error` was false + for every subtype instance. `message` is now defined non-enumerable + (the spec step was quoted in a comment directly above the enumerable + `setprop` it contradicted), so `Object.keys(err)` is `[]` as in node. +- **DataView indexed the buffer** (`runtime/ejs-typedarrays.c`): + DataView is not an integer-indexed exotic object; `view[i]` is an + ordinary property. The five custom specops are gone (typedarray5). +- **typed-array RangeError message** aligned with node: + `Invalid typed array length: N` (typedarray4). + +## Real divergences pinned (xfail), for a later burn-down + +- **fundecl1.js** — block-level function declarations hoist with + pre-ES6 web semantics (last decl wins at function entry); ES2015 + Annex B.3.3 gives whu/hi/whu/bye. +- **toLocaleString3.js** — `Number.prototype.toLocaleString` lacks + ICU's default maximumFractionDigits=3 rounding (node 1.236 vs ejs + 1.2355). +- **tostring5.js** — `Date.prototype` is an ordinary object in ES2015+ + (node throws on `Date.prototype.toString()`); ejs still gives it a + [[DateValue]]. + +All three "passed" before against ancient baselines. + +## Un-pinned by the value harness + +- **number1.js** (xfail removed) — the disagreement was purely about + how engines inspect `new Number(5)`; the shim prints `[Number: 5]` + on both sides. +- **date3.js** (xfail removed) — the off-by-an-hour was in ejs's date + STRING formatting, which the shim no longer consults; the epoch + values agree with node (under TZ=UTC and, at least currently, PDT). +- **esprima1.js** → `generator: none`: babel-node cannot transpile the + external-deps esprima-es6 ESM under babel-register (this was silently + broken under the old harness too — its baseline was unregenerable). + The output is `JSON.stringify` of the AST, engine-neutral; the + checked-in baseline equals ejs's (and esprima-under-ejs's) output. + +## Baseline churn (the whole point: it's small) + +Only 12 baselines changed content, and each is an ancient-node zombie +flushed: sparse arrays (`, , ,` → `<4 empty items>`, array21), V8 math +precision improvements ejs's libm already matched (math1), function +name rendering (object9/16), plus the tests named above. 19 baselines +are newly committed (tests that had none and regenerated in CI each +run, eir-*/types-* among them); 13 deleted were orphans or baselines of +non-globbed `-t`-only tests, which regenerate on demand. + +## Gates + +- stage0/1/2/3 suites: 424 pass / 21 xfail / 0 fail each + (453 globbed = 424 + 21 + 8 skip-if-at-runtime) +- test-stage1-shapes-off: 424 / 21 / 0 +- test-eir-lowtier: OK +- test-eir: the standing 11 compiler-P1.1 pins only, no new reds +- cross-version: baselines byte-identical from node 22.4.0 and 22.23.2 + +## Notes / follow-ons + +- `Object.keys` on arrays omitting index keys is a real ejs spec gap + (worked around in the shim); candidate for a future pinned-bug round. +- babel-node remains the generator for import-syntax tests; P7.4's tsc + move is the eventual owner of that dependency. +- The 11 test-eir reds are compiler-P1.1's, tracked there. diff --git a/docs/runtime-p4-results.md b/docs/runtime-p4-results.md new file mode 100644 index 00000000..e8dcd18e --- /dev/null +++ b/docs/runtime-p4-results.md @@ -0,0 +1,128 @@ +# runtime-P4 results — collector structural refactor (plans P6.3) + +Phase record for runtime-plan.md's runtime-P4: the cell-lifecycle +consolidation, explicit mark epochs, the root registry, the single +collection-policy function, and the ejs-gc.c file split. Landed +2026-07-29 on `eir`. Behavior-preserving by design; the differential +lanes gate. + +## What landed + +Three commits, each independently green: + +1. **Cell lifecycle in one block; explicit mark epochs.** The + scattered `SET_*`/`IS_*` color macros and the mutable + white/black mask pair became one section of inline functions + (`cell_is_free/gray/white/black`, `cell_set_*`) with the bitmap + encoding private to it. White/black are now EPOCH-RELATIVE: the + color bits hold GRAY or the parity of the mark epoch the cell was + last colored in, and `mark_epoch_advance()` — one call site, the + end of a full collection — ages every surviving black cell white in + O(1). The mask swap was the same aging as two coupled globals + mutated in place; the epoch is that flip made explicit and + single-owner. The dead `CONCURRENT` CAS macro variants went with + it. + +2. **Root registry; one collection-policy function.** The root set + is a growable array (O(1) add, swap-with-last remove) with ONE + iteration helper — full-GC mark, minor evacuation, compaction + fixup, the debug walks, and the shutdown NULL-out all go through + `root_registry_foreach`/`root_registry_shutdown` instead of five + hand-rolled walks of a malloc'd linked list. Every collection the + runtime initiates for itself is decided in `gc_policy(event)`: + the growth trigger on the old-allocation path, the post-minor + promotion check, the EVERY_N_ALLOC stress cadences (minor in + nursery mode, full in old mode), and the forced allocation-failure + collections. Each event preserves its historical baseline/counter + resets exactly, so collection schedules are unchanged. + +3. **File split.** ejs-gc.c (~3.7k lines) became six files plus the + internal contract header `ejs-gc-internal.h` (module map lives + there): + + | file | contents | + |---|---| + | ejs-gc.c | lifecycle API, allocator entry, cell free path, root registry, collection policy, GC JS object | + | ejs-gc-heap.c | arena reservation, arenas/pages, LOS + sorted-range lookup, find_page_and_cell | + | ejs-gc-mark.c | worklist, precise + conservative scanners, gc-frame skip, generator stack bookkeeping | + | ejs-gc-minor.c | the nursery and the mostly-copying minor | + | ejs-gc-major.c | full collections: mark/sweep orchestration, major compaction, the epoch advance | + | ejs-gc-debug.c | EJS_GC_PROFILE / WATCH / VERIFY / PARANOID | + + The split was verified mechanically: every function body extracted + from the old file and diffed against the new tree — 98/98 identical + modulo `static` (the two exceptions: `_ejs_gc_collect_inner` + gained the `root_registry_shutdown()` call; dead-code + `page_list_count` was dropped). The duplicate tentative + definition of `heap_size_at_last_gc` collapsed to one. + +## The two bugs the split surfaced (both pre-existing) + +The TU split shifts codegen — frame layouts, spill slots — and the +stress lanes promptly caught two hazards that ACCIDENTAL conservative +pins of stale stack copies had been masking. Both fixed; both are +the same lesson as gc-P4's bistable pin-scan: anything that depends +on C-stack luck is a latent bug. + +- **Orphaned old slot storage** (ejs-object.c). When + `shaped_ensure_capacity` grows a shaped object's out-of-line slot + array (or `_ejs_object_to_dictionary` drops it), an OLD-gen env + cell is disconnected while still holding its pre-copy slot values. + It is garbage until the next full sweep — but the old-gen WALKERS + (the minor's remset-overflow fallback, EJS_GC_VERIFY, + EJS_GC_PARANOID) cannot tell garbage from live and visit those + stale slots after the young referents move or die; the overflow + fallback could even "evacuate" a poisoned cell. Observed + concretely: the promoted env of the still-young rooted Reflect + object, orphaned by capacity growth during `_ejs_init`, whose slot + 7 aborted EJS_GC_VERIFY once the split removed the rescuing pin. + Fix: `shaped_retire_slots` queues the retiree for one precise scan + (remset entry) at retirement — the next minor rewrites its young + refs while they are still live, after which the cell is inert until + swept. + +- **Paranoid checker self-scan** (ejs-gc-debug.c). The + dying-young-referrer report's raw C-stack sweep scanned from its + own frame to stack bottom, which includes the COLLECTOR's frames — + written after the conservative pin scan ran. The sweep loop's own + cell cursor spilled into the probed range and reported the dying + cell as "still referenced." Fix: the sweep floors at the minor's + entry frame (`paranoid_stack_floor`, set in + `_ejs_gc_minor_collect`), so only frames the conservative scan + could have seen at pin time are probed. + +## Notes + +- The runtime-plan entry also listed "aligned LOS regions with + O(log n) lookup." The lookup half landed in gc-P4 (the sorted + range array + the arena direct map); with the 256-byte size class + (gc-P5) routing every cap-14 shape and >14-slot env to pages, the + LOS population is small and cold, so the alignment half is dropped + as moot. Raising the shaped field cap past 14 is shapes-plan + business (a behavior change), not this refactor's. +- The gc-P5 note about `old_alloc_cell_for_promotion` walking the + free-page list (61 profile samples) stands as a recorded perf item; + it was left alone here to keep the phase strictly + behavior-preserving. + +## Gates + +- Matrix: test-eir-lowtier + stage0–3 (including the stage2/stage3 + byte-identity fixed point) + stage1-shapes-off green. +- **test-eir was found RED at phase entry** — 11 failing EIR unit + tests, all pre-existing compiler-side test debt from gc-P5 part 2's + flag-off born-shaped literals: stale `make_object keys=[...]` + expectations, a stale "flag-off keeps make_object" test, and the + sinking/sink-flow fold tests, which fail knob-independently (the + flow-sensitive sinking does not drain `make_object_shaped`, and + `assertNotContains("make_object")` substring-matches the shaped op + besides). Proven pre-existing: this phase touches no lib/ file + (`git diff` empty against the base commit for lib/), and the + failures reproduce from the base commit's sources alone. Recorded + in compiler-plan territory as follow-up; NOT masked by editing the + tests here. +- The gc stress lane (the gc tests × EVERY_N_ALLOC 7/31/101 × + PARANOID / VERIFY / NURSERY=off / COMPACT=off) matches the + phase-entry baseline failure set exactly — the pre-existing pinned + generator-stress bugs (runtime-P1's burn-down list) and nothing + else. Verified identical at every intermediate commit. diff --git a/docs/runtime-plan.md b/docs/runtime-plan.md new file mode 100644 index 00000000..a27001f6 --- /dev/null +++ b/docs/runtime-plan.md @@ -0,0 +1,111 @@ +# runtime-plan: correctness burn-down and runtime features + +Bucket plan; the ordering spine lives in `docs/plans.md` (milestone +references look like `runtime-P1`). This bucket owns the pinned +runtime bugs (found by the differential harnesses and pinned by tests, +deliberately not fixed mid-phase) and runtime-side features that no +performance bucket owns. + +## Phases + +- [x] **runtime-P1 — Pinned-bug burn-down.** All ten fixed; DONE + 2026-07-29 — docs/runtime-p1-results.md (each entry there + records what the bug actually was): + - `typeof null` → `"object"` (runtime + compiler fold + + typeof_is helpers; typeof_is_object also stopped admitting + functions). + - `-0 === 0` → true (strict_eq compares numbers before the + NaN-box tag; same flaw fixed in loose eq, SameValue — which + returned false for `Object.is(0,0)` — and SameValueZero). + - `Math.round` ties toward +∞. + - `Number(" 7 ")` → 7 (real StringToNumber: ES whitespace + trim, "Infinity" only, 0x/0b/0o, empty → 0). + - `-8 >>> 28` → 15 (shifts + ToUint32 had UB double→unsigned + casts; shifts also coerce non-number operands now). + - `1 + null` → 1 (ToNumber(null) = 0; add's string test moved + to the ToPrimitive results). + - `"a" * "b"` → NaN (mult/div/mod are ToNumber-both-sides). + - Uncaught generator-body throw propagates to the caller + (invoke_closure_catch at the body boundary; resume sites + rethrow on the caller's stack). + - Sparse-array element storage implemented (aligned 512-slot + arraylets); sparsearray1.js un-xfailed. + - `getOwnPropertyNames`: non-enumerables included, primitives + ToObject-coerced, array/String index properties + `length` + reported. +- [x] **runtime-P2 — Export-boundary wrapper.** Escaping entry points + previously pinned down specialization entirely (the compiler + buckets declined them). DONE 2026-07-29 — + docs/runtime-p2-results.md. What landed, and why the shape + differs from the original sketch ("dispatching to + specialized/trusting internals"): + - maam's value domain is CONSTANT-PROPAGATION, so its claims + about an escaping function's body may hold only for the + argument constants it analyzed — boundary tag guards cannot + re-establish them for external callers. The wrapper therefore + dispatches to an UNTRUSTED clone: f64 formals boxed once at + entry (the optimizer's structural number proof), ordinary + guarded diamonds inside (assume-and-guard gate), boxed result. + The formal-rooted diamonds fold trust-free to trusted-clone + quality — types-bench5 (exported kernel, cross-module hot + loop) runs at PARITY with the closed-world trusted path. + - the same analysis-coverage argument exposed a PRE-EXISTING + cross-module miscompile: trusted rewrites inside escaping + functions consumed claims external callers can violate + (types-wrapperfence1 pins it: a constant-pruned branch + + an external 7 → unguarded unbox of a string). Fixed by the + escape-taint fence: taint = escaping closures, closed under + callee-of-tainted-hosted-site and created-in-tainted-host; no + trusted clone for escapees, no trusted rewrite of + tainted-hosted sites. Covered (untainted) code runs only + during module init — before an external caller can exist — so + its trusted machinery keeps its whole-program justification + (residual, documented: an import cycle can re-enter mid-init; + not modeled). + - EJS_NO_EXPORT_WRAPPER bisects the wrapper; the fence has no + off-switch (it is a soundness fix). Follow-on recorded: + wrappers/guarded dispatch for tainted-called internal helpers, + and a payoff gate that credits call-heavy bodies (a bare + delegation export currently declines). +- [x] **runtime-P3 — Value-based test harness.** Test baselines were + generated live by `node ` and were sensitive to node's + console.log inspect-format drift (22.4 → 22.23 changed array + formatting); CI pinned node 22.4.0. DONE 2026-07-29 — + docs/runtime-p3-results.md. What landed: + - `test/harness-console-shim.js`: a harness-owned value + serializer replaces console.log on BOTH sides (node generation + via harness-run.js, ejs via a compiled import wrapper), so + baselines assert on values; node 22.4.0 and 22.23.2 generate + byte-identical baselines and CI floats on `22.x`. TZ=UTC + pinned by the tester. + - the stale-baseline un-masking flushed real bugs: FIXED — + native error prototypes had null [[Prototype]] (instanceof + Error was false for subtypes; message now non-enumerable), + DataView wrongly indexed its buffer, typed-array RangeError + message aligned with node. PINNED (xfail) — Annex B.3.3 + block fundecl hoisting (fundecl1), toLocaleString ICU + rounding (toLocaleString3), Date.prototype-is-ordinary + (tostring5). UN-PINNED — number1, date3; esprima1 is + `generator: none` (babel-register never could transpile the + external-deps ESM). + - tester fixes: the scheduler silently skipped the test at index + test_threads in both passes (weakmap2 had never actually run); + per-test compile TMPDIRs (shim module made concurrent compiler + temp names collide); baselines regenerate when the harness + itself changes. + - gates: stage0-3 + shapes-off all 424/21/0, lowtier OK, + test-eir = the 11 standing compiler-P1.1 pins only. +- [x] **runtime-P4 — Collector structural refactor.** Recorded during + the gc-P2 debugging sessions, deliberately deferred while phases + were landing: extract a cell-lifecycle module (alloc/free/color + in one place), kill the mark-color mask flip in favor of explicit + epochs, aligned LOS regions with O(log n) lookup (also unblocks + raising the shaped-object field cap past 14), a real root + registry API, one collection-policy function, and a file split + (ejs-gc.c is ~3k lines). Behavior-preserving; gated on the + differential lanes. DONE 2026-07-29 — + docs/runtime-p4-results.md (the LOS lookup had already landed + with gc-P4; the cap raise is shapes-plan business). Flushed two + pre-existing stack-luck hazards: orphaned old slot-storage envs + (retirement now queues one precise scan) and the paranoid + checker's self-scan of collector frames. diff --git a/docs/shapes-plan.md b/docs/shapes-plan.md new file mode 100644 index 00000000..e206f530 --- /dev/null +++ b/docs/shapes-plan.md @@ -0,0 +1,966 @@ +# Shapes: shape-guarded property access, co-designed with the GC (maam P4) + +Phase ids here are `shapes-P1`..`shapes-P6` (formerly maam-P4.1..P4.6 — +the bucket was born as maam's fourth phase; commit messages and results +docs use the old ids). The ordering spine lives in `docs/plans.md`. + + +This is the maam-plan **P4 design document** — the phase the plan scoped as +"design doc only" and deferred "until Phase 3 has proven the pipeline." +Phase 3 has: typed arithmetic (P3), trust-free guard-region optimization +(P3.4), the differential harness that validates the oracle against concrete +execution (P3.5), and function specialization with native unboxed +signatures (P3.6, types-bench1 ~46×, hypot2 demo ~90×). What is left on +the table after all of that is exactly one thing: **the object model**. +Every property access is still a hash lookup through an out-of-line +malloc'd map behind two indirect calls, every object literal is built by N +generic inserts, and the P3.6 benchmarks' residual wall time is boxed slot +traffic. Shapes are where that goes away, and — per gc-plan.md §"Object +header, forwarding, and shapes" — they are "the single highest-leverage +item" in the GC redesign too. This document is the joint design the two +plans each point at, written against the GC plan's Phase 1 header layout, +as instructed there ("The GC must not ship a header layout that shapes +then has to break"). + +Deliverables of this doc: the runtime shape model, the object layout +migration, the EIR ops and their verifier/trust rules, how the oracle's +`layouts()`/`constructors()` facts are consumed and *when they may be +trusted* (the promotion criteria, distilled from Phase 3 experience), a +phased implementation checklist with gates, and the validation story. + +## What we have today, as found + +Runtime object model (all file:line refs current at this writing): + +- `EJSObject` = `{ GCObjectHeader gc_header; EJSSpecOps* ops; ejsval + proto; EJSPropertyMap* map; }` (`runtime/ejs-object.h:229-234`). The + header is a bare `uint32_t` (`ejs-types.h:30`) whose low bits are the + `EJSScanType` and whose high byte holds user flags (extensibility) + (`ejs-gc.h:14-23`, `ejs-object.h:220-227`). On 64-bit targets 4 bytes + of padding follow it. **No inline property slots exist.** +- The property map is a per-object, malloc'd (non-GC-heap), separately + chained hash table (`ejs-object.h:116-136`) whose entries point at + individually malloc'd `EJSPropertyDesc` descriptors — flags + value or + getter/setter (`ejs-object.h:13-40`). Insertion order is a second + linked list threaded through the entries (enumeration depends on it). + It rehashes through a prime ladder and aborts past 4099 buckets + (`ejs-object.c:388-391`). +- A property get is: `_ejs_object_getprop` → `ToObject`/checks → indirect + `OP(obj,Get)` → `ToPropertyKey` (may `ToString`) → indirect + `GetOwnProperty` → hash, modulo, bucket-chain walk with + `_ejs_op_strict_eq` per candidate — repeated per prototype level + (`ejs-object.c:830-863`, `2039-2094`). A property add is a descriptor + malloc + map insert (`ejs-object.c:533-572`, `2098-2168`). +- Allocation is `ops->Allocate()` = `_ejs_gc_alloc(sizeof(EJSObject))` + followed by `_ejs_init_object`, which **calloc's the map** — every + object is two allocations, one outside the GC heap + (`ejs-object.c:752-772`, `2400-2403`). +- There are **no hidden classes, no shapes, no inline caches** anywhere + in the runtime (grep-confirmed). Class identity is the `ops` pointer. +- Compiled code's view of `EJSObject` lives in `lib/types.ts:88-103` and + must move in lockstep with any runtime layout change (the gc-plan's + atomic-land rule). + +Compiler/oracle side: + +- maam computes **type-aware hidden classes**: a `Shape` is an interned + *set* of `(name, TypeSig)` fields — order-insensitive by design (an AOT + compiler picks its own layout; order-sensitivity cost splay 109,603 + shapes vs 258), hash-consed with stable ids, with a megamorphic `⊤` + under a per-address cap (`echojs-maam/src/lang/shapes.ts`). +- `layouts()` reports, per allocation site, the **terminal** shapes its + objects settle into (construction intermediates are subsumed away) plus + a C-style struct layout per shape under a pluggable size model; + `monomorphic` = exactly one terminal shape. `constructors()` gives the + same per `new F()` callee. `accessorSites()` marks getter/setter + dispatch sites and their target sets (`echojs-maam/src/layout.ts`, + `src/analysis.ts:38-97`). All of these are **`Loc`-keyed**; the + node-identity discipline the compiler consumes (`typeOfNode`, + P1's oracle contract) does not cover them yet. +- EIR lowers property access to `get_prop_atom`/`set_prop_atom` + (GENERIC_OP runtime calls), and `make_object` to `_ejs_object_create` + plus one full generic `_ejs_object_setprop` per key + (`lib/eir/emit.ts` "make_object"). + +## The design in one paragraph + +The runtime grows a global, interned, **type-aware shape tree** that +mirrors maam's abstraction one-for-one; every ordinary object carries a +shape index in its (gc-P1-widened) header and stores its plain data +properties in a **slot array at shape-determined offsets**, falling back +to today's map ("dictionary mode") the moment anything exotic happens — +deletes, non-default attributes, symbol keys, cap overflow. The compiler, +exactly as in Phase 3, consumes oracle shape facts **guarded**: a property +access on an oracle-monomorphic receiver lowers to a `has_shape` diamond +whose fast arm is a fixed-offset slot load/store (typed, when the shape's +field representation says so) and whose slow arm is today's generic call — +correct regardless of oracle accuracy, because the guard decides at +runtime. Allocation sites with known terminal shapes are **born with +their shape** (one sized allocation, direct slot stores, no map, no +descriptor mallocs) under the same structural fences P3.6 built for +specialization. The GC's Phase 5 then consumes the same shapes for +per-shape trace bitmaps and memcpy evacuation; nothing in this document +waits for the mover, and nothing here may break its header. + +## Runtime design + +### Shapes are type-aware, and mirror maam exactly + +A runtime shape is `(parent, name, repr)` — a transition edge appended to +a parent shape, where `repr` is the field's representation: one of +`{unboxed-f64, boxed}` initially (finer tags later if profitable). maam +made representation part of class identity because an AOT compiler gets no +deprecate/migrate second chance; the runtime must agree, or a compiled +guard could pass while the field representation lies. A type-changing +store (`o.x = "s"` where x was num) is therefore a **transition** like a +property add: the object moves to the sibling shape with `x: boxed`, and +compiled fast paths guarding the old shape correctly fail to the generic +path. This is the load-bearing choice of the whole design: **a passed +shape guard proves both structure (offset) and representation (how to +load)**, so in typed regions a field the oracle typed `num` is one +compare + one 8-byte load away from a raw `double` — no `has_tag`, no +unbox — and the P3.4/P3.6 raw-value machinery applies unchanged +downstream. + +Two deliberate divergences from maam's shape table, both mechanical: + +- **Insertion order.** maam interns order-insensitively; ES enumeration + is insertion-ordered, and today's runtime honors that via the map's + insert list. Runtime shapes get order for free — the transition chain + *is* the insertion order — so enumeration walks the shape's field + chain. The correspondence rule for the compiler: a *runtime* shape is + an ordered witness of a maam shape (same field set, same reprs). The + compiler must therefore know the ORDER, not just the set, to intern the + guard's expected shape — see "deriving ordered shapes" below. +- **Interning is global and cross-module.** One process-wide shape + table, append-only, sharded by parent (transition lookup: + `parent × name × repr → child`, one hash hit per property add). Shape + ids are stable within a process, NOT across processes or modules at + compile time — compiled code never embeds a numeric id. Instead each + module interns the shapes it guards on at module init (exactly the + atom-table precedent: `getAtom`/`_ejs_module` machinery) and guards + compare against the module-global's loaded value. Cross-module + structural identity falls out of interning. + +### Object layout, in two steps + +**Step A (before gc-P5, works on today's non-moving collector):** + + EJSObject: + u32 gc_header (unchanged low bits: scan type, user flags) + u32 shape_index (the gc-P1 reserved bits; 0 = dictionary) + EJSSpecOps* ops + ejsval proto + union { EJSPropertyMap* map; // dictionary mode (shape 0) + ejsval* slots; // shaped mode: GC-heap slot array + } + + The shape index takes the 4 padding bytes the gc-plan's Phase 1 + earmarks (gc-plan.md:291-300) — this doc claims 24 bits of them for the + shape index plus a mode bit; forwarding/age/mark/card bits own the + rest, allocated jointly with gc-P1 in one atomic + `runtime/` + `lib/types.ts` change. If shapes land before gc-P1, the + same commit simply widens the header first and gc-P1 inherits it; the + two plans agreed this is one layout, written once. + Slot arrays are GC-heap allocations (`EJS_SCAN_TYPE` of their own, + scanned as ejsval ranges — precise tracing needs no per-shape bitmap + yet), sized to the shape's field count rounded to the allocator's size + class, grown by copy on transition past capacity. Objects lose the + calloc'd map entirely in shaped mode; dictionary mode keeps today's map + code verbatim. + +**Step B (gc-P5, the fused future):** slots move inline — +`shape id + contiguous inline slots`, fixed-size, memcpy-copyable, traced +by per-shape pointer bitmaps, born from the bump allocator. Nothing in +this document's compiler-visible contract changes at that point except +the addressing base (slot array pointer → object-interior offset); the +EIR ops below deliberately take a slot *index* immediate so the emitter +owns that switch. + +### Semantics: what is shaped, and what falls back + +Shaped mode covers **plain data properties with default attributes +(writable, enumerable, configurable) and string keys on ordinary +objects** (`ops == &_ejs_Object_specops`). Everything else is dictionary +mode, entered by a one-way `to_dictionary(obj)` migration (allocate map, +insert fields in shape order, shape_index := 0): + +- `delete` of a shaped field (`ejs-object.c:2199-2220` path); +- `Object.defineProperty` with any non-default attribute, or + data↔accessor conversion (`ejs-object.c:2224-2397`); +- accessor definition (`ejs-object.c:886-903`); +- symbol keys; numeric/index keys (arrays keep their own storage; + indexed access on plain objects is rare enough to eat the map); +- transition-cap overflow (a per-object add-count cap, the runtime twin + of maam's `shapeCap`) and any shape-table pathology; +- `preventExtensions`/`freeze`/`seal` keep shaped mode (they only toggle + the extensibility flag and attribute bits conceptually — but a + non-writable field breaks the "plain store" invariant, so freeze/seal + ALSO migrate; `preventExtensions` alone does not). + +`[[Set]]` on a shaped object: field present with same repr → slot store; +present with different repr → transition (sibling shape, same offsets, +new repr), then store; absent + extensible → transition (append), grow +slots if needed, store; anything else → migrate, then today's path. +`[[Get]]`/`GetOwnProperty` on shaped objects synthesize the default +descriptor from the slot; the specops keep their signatures — shapes are +an implementation detail *behind* `_ejs_Object_specops`, invisible to +every other class and to the spec algorithms above it. Proto mutation +(`__proto__` setters, `_ejs_object_literal_set_proto`) does not affect +the shape (shapes describe own-property structure only; proto stays a +per-object field), so no Crankshaft-style proto-in-shape complexity. + +The insertion-order list, `OwnPropertyKeys`, `Enumerate`, and the +for-in iterator read shaped objects by walking the shape chain +(`ejs-object.c:639-660`, `1103-1118` become mode-switched); the +scan/finalize specops likewise (`ejs-object.c:2406-2434`). + +### Born with their shape + +`make_object` at a site whose literal keys are static becomes: intern the +ordered shape at module init; allocate object + slot array in one runtime +call `_ejs_object_new_shaped(shape, proto)`; store each value at its +fixed offset (initializing stores — barrier-elidable when gc-P2 lands). +This is correct *unconditionally* for object literals — the literal's key +order and count are the site's static truth, no oracle involved; the +oracle only adds field *representations* (typed slots) and the terminal +shape when later code appends more fields. Constructor bodies are the +oracle-and-fence case: see promotion criteria. + +## EIR design + +### Ops + + // i1: does obj's shape index equal the module-interned shape? + // imms.shape names the module's shape-table entry (a link-time + // global, like atoms). Effect NONE — a pure header compare. + has_shape: { arity: 1, effects: NONE, imms: ["shape"], + sig: { params: ["ejsval"], result: "i1" } } + + // fixed-slot access. imms.slot is the field index within the + // guarded shape (the emitter turns it into slot-array/inline + // addressing); imms.repr ∈ {"boxed","f64"} selects the load/store + // type — "f64" produces/consumes raw f64 (the P2 typed-flow rules + // apply; only reachable behind a has_shape proving that repr). + slot_load: { arity: 1, effects: READ, imms: ["slot", "repr"] } + slot_store: { arity: 2, effects: WRITE, imms: ["slot", "repr"] } + + // allocation with a known shape: operands are the initial slot + // values in shape order (imms.shape, imms.reprs). GC|WRITE like + // make_object; replaces make_object at statically-shaped sites. + make_object_shaped: { arity: -1, effects: GC|WRITE, + imms: ["shape", "reprs"] } + +Verifier rules, in the P2/P3.4/P3.6 lineage: `slot_load`/`slot_store` +with `repr:"f64"` produce/take raw f64 and are subject to the existing +raw-values rules; a `slot_*` op must be dominated by a `has_shape` on the +same value for the same shape **when carrying `repr:"f64"`** (the boxed +case is memory-safe under any shape of at least `slot+1` fields, but the +verifier still requires the guard — structural discipline over cleverness, +same as rawJoin's re-checked marker). `has_shape` on a non-object value +is simply false at runtime (the emitter folds the NaN-box object check +into the shape-index load exactly as `isNumber` backs `has_tag`). + +### Lowering: the shape diamond + +`o.x` where the oracle types `o`'s site monomorphic with terminal shape S +(and `x` present in S): + + %t = has_shape %o, shape="S" + cond_br %t -> ^fast, ^slow + ^fast: %v = slot_load %o, slot=k, repr=… (raw f64 when S says num) + ^slow: %g = get_prop_atom %o, atom="x" (today's generic call) + join boxed — or raw, via the existing rawJoin machinery + +— the same diamond skeleton as `numericDiamond`, the same join +conventions, the same "guarded consumption is correct even when the +oracle is wrong" contract. Stores dual. `optimize-guards.ts` extends +its dominator facts: a dominating passed `has_shape %o, S` proves (a) +later `has_shape %o, S` guards fold, (b) `%o`'s field reprs — so a +`slot_load repr:"f64"` needs no `has_tag`, and consecutive accesses to +the same receiver merge into ONE guard region with one slow path, +exactly the hypot2 shape. SSA immutability makes receiver-value facts +sound the way number-ness was; **stores are the new wrinkle**: a +`slot_store`/`set_prop_atom`/call/construct between accesses can +transition the receiver's shape, so shape facts are killed by +WRITE|CALL-effect instructions on any path — the fact table gains an +effect-kill rule the number facts never needed. (A same-region +`slot_store` that does not add a field and matches repr provably does +NOT transition — the one exception the fact engine may keep.) + +`new F()` with a monomorphic `constructors()` report lowers `construct` +unchanged (the runtime allocates via F) in the guarded phase; +born-with-shape construction is a promotion (below), not a lowering +default. + +### Oracle interface additions (maam side, small) + +The compiler consumes shape facts through the node-identity discipline +every prior phase used; `Loc`-keyed tables don't survive the desugar +pipeline's node surgery. maam grows (mirroring `nodeTypes()`): + +- `layoutOfNode(objectLiteralNode)` → SiteLayout | undefined; +- `constructorReportOfNode(fnNode)` → ConstructorReport | undefined; +- `receiverShapesOfNode(memberExprObjectNode)` → Shape[] — the shapes + the *receiver value* of a property access may have (join over reached + configurations), which is what access-site guarding actually needs + (allocation-site layouts alone don't cover parameters/loads); +- ordered-shape witnesses: for literals the compiler orders fields + itself; for constructor reports maam must ALSO expose the terminal + shape's field order as first-write program order per analyzed path, or + decline (order ambiguity ⇒ no born-with-shape, guards still fine since + guards compare interned ordered shapes the RUNTIME built — see open + question 1). + +The compiler-side `TypeOracle` (lib/eir/oracle.ts) grows the same three +queries plus pass-through of `shapeCapHits`/megamorphic flags for the +promotion gates. `--types-dump` grows a per-site shape census +(diagnostics first — the plan's original P4 note — which doubles as the +instrumentation the shapes-P1 gate needs). + +## Promotion criteria — what Phase 3 taught us + +The trust ladder, restated as policy for shapes: + +1. **Guarded by default.** Shape diamonds are emitted wherever facts are + *exact* — monomorphic, non-megamorphic, `shapeCapHits==0` for the + site, every guarded field's repr a single tag. Wrong oracle = slow + path taken = speed lost, never correctness — the P3 contract. +2. **Exact facts only, no near-misses.** Three or more terminal shapes + ⇒ no diamond; exactly two lower to the shapes-P6 2-way chain (measured and + landed — see the shapes-P6 entry), and only when EVERY shape in the answer + passes the same exactness screen and carries the accessed field; + union-repr fields load boxed; anything the oracle degraded + (`degradedBindings`, unknown calls touching the receiver) declines. + This is `operandIsNumber`'s "exactly {number}" rule transplanted. +3. **Unguarded consumption only behind structural fences.** + Born-with-terminal-shape construction asserts facts (in-ness of + not-yet-assigned fields is observable: `"b" in this` mid-construction + must be false, but a terminal-shape-born object would say true). So + it requires the P3.6 fence pattern, compiler-side and oracle-free: + the constructor's `this` never escapes before the last field store + (no calls, no stores of `this`, no `in`/`delete`/enumeration — a + straight-line store prefix), checked structurally on the lowered EIR + like the escape analysis checked closures. Object literals need no + fence (their construction is atomic in the source). P3.6 clones may + additionally drop shape guards on receivers their own escape analysis + proves site-local — later, measured, never first. +4. **Trust-free optimizer, provenance-not-trust markers.** Guard-region + merging and fact folding must re-verify structure (the P3.4 verifier + discipline: a marker can tighten checking, never admit); the + effect-kill rule for shape facts is part of the verifier's soundness + inventory from day one — it is THE new hazard class this phase adds, + and the adversarial-review focus (P3.4's review found 4 miscompiles + in exactly this kind of machinery; assume this phase's review will + too). +5. **Visible degradation.** Every declined promotion has a counted + reason (`shapes: declined polymorphic=N megamorphic=M capped=K + escaped=E`), printed on the stats line; the diff-lane scrapes stay + additive-only. +6. **Bisect hooks per mechanism.** `EJS_NO_SHAPE_GUARDS`, + `EJS_NO_BORN_SHAPED`, runtime `EJS_SHAPES=off` (dictionary-only mode) + — the EJS_NO_EIR_OPT/EJS_NO_EIR_SPEC mold. + +## Validation + +- **The differential harness is the precondition again.** Before any + unguarded consumption (born-with-shape), the P3.5 harness grows a + shapes lane: per allocation site, the concrete machine's object + field-sets must be contained in the abstract terminal+intermediate + shape sets (`abstract ⊒ concrete`, the containment-lane pattern), and + `ejs` runs must agree with node on shape-sensitive observables + (`Object.keys` order, `in` during construction, delete-then-readd, + freeze/seal, accessor conversion). Guarded-phase work (shapes-P3) does not + wait for this; born-with-shape (shapes-P4) hard-requires it — the P3.5/P3.6 + sequencing, replayed. +- **Runtime differential mode.** shapes-P1/shapes-P2 land behind `EJS_SHAPES=off`; + the whole test suite runs both modes and byte-compares (the old- + collector A/B discipline from gc-plan). A transition-storm stress test + (add/delete/type-flip churn) and the collect-every-N stress compose. +- **The --types diff lane** stays the behavioral gate for every compiler + phase, unchanged: flag-off untouched, `--types` byte-identical stdout. +- **Wrong-oracle probes.** A probe whose runtime shape diverges from the + oracle's claim (cross-module mutation of a "monomorphic" site's object, + the types-wrongoracle1 pattern) must route through the guard's slow + path with identical output; a looks-fenced-but-isn't constructor (mid- + construction escape via a call) must be *rejected by the structural + check*, pinned at unit level with a lying stub oracle — the P3.6 + wrong-oracle discipline, transplanted. +- **EIR-shape unit tests** for every op/verifier rule (guard-dominance + for f64 slots, effect-kill of shape facts, merge refusals), and probes + in test/types/ with census entries. + +## Benchmarks + +- **types-bench2** (new): constructor + field-access kernel — allocate N + points in a loop, sum `p.x*p.x + p.y*p.y` — the object-model twin of + types-bench1; measured at every phase gate (guarded, born-shaped, + typed-slots deltas recorded like 10.3×→14.0×→46× was). +- **splay** (the shape-stress classic; maam's own shape work was tuned on + it) as the polymorphism/transition stress once shapes-P2 lands. +- The gc-plan Phase 0 allocation profile doubles as the object-size/ + field-count census that sizes slot-array classes. + +## Phased plan + +Same bias as eir/maam/gc: small phases, matrix green after each, each +revertable, runtime phases A/B-able against the old path. + +- [x] **shapes-P1 — Runtime shape tracking, behind the scenes.** DONE + 2026-07-23. Shape table + transition cache + (`runtime/ejs-shapes.{h,c}`); ordinary objects get shape indices + maintained on insert/delete/type-flip; the MAP REMAINS the store + (dual bookkeeping, zero behavior change); `EJS_SHAPES=off` kills + it, `EJS_SHAPES_CENSUS=1` dumps the census at exit, + `EJS_SHAPE_CAP` overrides the per-object field cap (default 64). + Header bits landed as the gc-P1 joint layout: `GCObjectHeader` is + now `uint64_t` (ejs-types.h documents the split — low 32 unchanged, + bits 32-55 shape index, bit 56 shapes-P2 mode bit, 57-63 reserved gc); + `EJSObject`/`EJSPrimString`/`EJSPrimSymbol` sizes unchanged + (padding absorbed), `EJSClosureEnv` +8; `lib/types.ts` mirrored in + the same commit (header as two i32 fields so shapes-P3's `has_shape` + can load the shape half directly). + *Gate results:* matrix green (test-eir, lowtier, stages 0-3); + stage1 suite green with shapes on AND under EJS_SHAPES=off — the + off-mode run is a standing buck lane, `//:test-stage1-shapes-off` + (buck-test-stage.sh grew a TEST_ENV arg; the shapes-P2 both-modes + byte-identical gate extends this lane); + property-insert micro-overhead **2.1%** (mean of 5 interleaved + runs, 300k objects × 8 fresh atom-keyed inserts — the worst case; + needed the header-inlined transition-memo fast path, which serves + 99.99% of bench transitions: a memo-hit name was vetted when the + memo's shape was interned, so the whole check collapses to one + ejsval compare). Census (3-site probe: literal loop, delete, + accessor, repr-flip): 1052 objects born tracked, 160 shapes + interned, max depth 56 (a runtime-init builtin), transitions 3206 + of which 95% memo hits, 1 repr flip, 42 migrations (attrs 28 / + accessor 11 / symbol-key 2 / delete 1 — runtime-init builtins + dominate; user objects stay shaped). Death census needs a + collection to fire (finalize-driven), so short probes report 0 + deaths — the shapes analog of gc-P0's numbers lands with real + workloads in the shapes-P2 gate. +- [x] **shapes-P2 — Slot storage for shaped objects.** DONE 2026-07-24. + The union flip landed: `EJSObject`'s fourth word is now + `union { EJSPropertyMap* map; ejsval slots; }` — shaped-mode + objects store plain data property values in a **closureenv** slot + array (already GC-allocated, ejsval-range-scanned, and traceable + via its ejsval tag: zero GC changes, `lib/types.ts` untouched + since the word stays pointer-sized and compiled code never + dereferences it). Slot storage is lazy (`_ejs_null` until the + first property; grow-by-doubling from 4), so ordinary-object + allocation lost the map calloc entirely. ejs-shapes.c became a + pure transition/query API (`_ejs_shape_lookup` / `_fields` / + `_transition_add(+memo fast path)` / `_transition_set`); the + storage engine and the one-way `_ejs_object_to_dictionary` + (materialize map from shape+slots, malloc-only, no GC points) + live in ejs-object.c. Specops mode-switched: get (fast-path slot + load), set (fast-path store on the receiver incl. repr-flip + transition), define (shaped routing for plain default-attr data + props; everything else migrates then falls into the untouched + generic algorithm), delete (migrate then map-remove), + GetOwnProperty (synthesizes the default data descriptor into a + 32-entry gc-rooted static ring — safe because every + descriptor-mutating path migrates first), scan/finalize, plus the + map-walking sites: collect_keys (for-in), OwnPropertyKeys (shared + classification loop keeps the two modes byte-identical), + getOwnPropertyNames/Symbols, Object.assign, defineProperties. + **The stage2 lesson (found at this gate, the hard way):** the + first cut hung stage2's self-compile for hours at 100% CPU inside + GC marks. Two causes, both fixed here: (1) slot arrays of + capacity 32+ exceed the page allocator's largest cell — which is + **128 bytes**, not the 256 its comment claims (`ffs(256)=9 > 8` + LOS-routes exact-256 allocations) — so every wide object's storage + landed in the LOS, whose **per-reference linear lookup** made + marking quadratic (multi-minute marks of a 183MB heap; lldb kept + landing on the los_list walk at ejs-gc.c:550). Fix: + `EJS_SHAPE_FIELD_CAP_MAX = 14` (16B env header + 14×8 = exactly + 128B; growth 4→8→14); 15+-field objects drop to dictionary mode. + Revisit when gc-plan gives the LOS an O(log n) lookup or a 256B + size class. (2) the collection trigger was a **fixed 60MB of + allocation** — quadratic total GC work on a growing live set now + that property storage lives in the GC heap. Fix in ejs-gc.c: the + trigger scales to max(60MB, post-sweep-footprint/2); programs + under 120MB footprint keep the old cadence exactly. With both + fixes stage2's self-compile completes normally (ejs-process CPU: + 92s shapes-on vs 62s off on the same binary — the ~1.5× is env + alloc churn plus wide-object migrate-through; the raw win arrives + with shapes-P3's guarded fast paths, and shapes-P5/gc-P5 own the layout + end-state). + *Gate results:* matrix green — test-eir, lowtier, stages 0-3, and + the `//:test-stage1-shapes-off` A/B lane (no kangax runner exists + in-repo; the stage suite + the new probe stand in). New + `test/shapes-storm1.js` transition-storm probe (adds, repr flips, + deletes, attrs/accessor/symbol/index migrations, freeze/seal, + enumeration order, assign/defineProperties/JSON): node-identical, + byte-identical across EJS_SHAPES on/off, and green under + EJS_GC_EVERY_N_ALLOC=7 in both modes. Microbench (300k objects × + 8 atom-keyed fields, 20 passes, interleaved runs, post-fix): + **set 3.2× faster** than the map (6.35s vs 19.9s — no hash, no + strict-eq chain, no descriptor churn), **get 1.09×** (5.95s vs + 6.47s; the generic-call overhead still dominates — the raw win is + shapes-P3's guarded fast paths), insert 8×N **~3% slower** (1.93s vs + 1.88s: one closureenv alloc + one grow-copy per 8-field object — + within the shapes-P1 <5% bar, and the shaped path now does real work + instead of dual bookkeeping). Census on the storm probe: 383 + born tracked, 315 shapes, 1365 transitions (48% memo fast hits), + 210 repr flips, migrations correctly attributed. +- [x] **shapes-P3 — Guarded fast paths under --types.** DONE 2026-07-24 + (gate results below). As built: + - **Ops** (`lib/eir/ops.ts`): `has_shape` (NONE, i1), + `slot_load` (READ) / `slot_store` (WRITE) with imms + `shape`/`slot`/`repr` — the ops carry the shape KEY too (a small + deviation from this doc's sketch) so the verifier compares + against the guard instead of inferring, and `Module.shapes` + (ir.ts) holds each module's interned field lists (`internShape`, + key = `name:repr,...` in insertion order — printed IR is + self-describing). + - **Verifier** (`verifier.ts`): the effect-kill soundness inventory + lives at the top of the file with the engine itself — + `computeShapeFacts`, a forward must-dataflow (facts born on TRUE + edges of same-block-fresh has_shape cond_brs, killed by every + WRITE|CALL, intersected at joins, dead across unwind edges). + Every slot op must sit under an un-killed fact for its exact + (value, shape); `slot_store` additionally needs a dominating + has_tag fact matching the field repr (true-edge for f64, + false-edge for boxed) OR — f64 only — a value-intrinsic number + proof (const/box_f64/mul-div-sub), because foldProvenGuards + legitimately deletes a has_tag on a proven number (found by the + wrong-oracle probe at this gate, fixed by mirroring the + optimizer's intrinsic proofs — dominance-fact folds never delete + the edge the store rule needs). Slot bounds + repr are checked + against Module.shapes. + - **Runtime** (`ejs-shapes.{h,c}`): `_ejs_shape_intern(nfields, + names, f64_mask)` walks/interns the ordered shape at module init + (the atom precedent); `EJS_SHAPE_NOMATCH` (0xFFFFFF) is reserved + (shape_alloc stops one short) so an unfilled/off-mode shape + global can never match any header — under `EJS_SHAPES=off` every + guard is false and the slow paths serve everything. + - **Emitter** (`emit.ts` + compiler.ts): has_shape folds the + NaN-box object check into the header-high-half compare against a + per-shape i32 module global (`isObject`/`objectPointer` live + beside isNumber in compiler.ts); `slotRef` is THE addressing + seam (shapes-P2 closureenv slot arrays today, gc-P5 inline slots + later); interns flush into the literal-init function's return + block after all atom inits (`emitShapeInterns`). + - **maam**: `receiverShapesOfNode` (terminal-filtered, node- + identity, fail-soft) + `fieldOrderOfShape` (the ordered witness = + first-interning insertion order; a runtime object built in + another order just misses the guard). `layoutOfNode`/ + `constructorReportOfNode` are shapes-P4 consumers and wait there. + - **Lowering** (`lower.ts` propGet/propSet): diamonds at every + atom-keyed member get/set incl. compound assign, ++/--, method + loads, and destructuring reads. Exact facts only (criterion 2): + monomorphic, non-⊤, shapeCapHits==0, all reprs single-tag, + ordered witness, field present — every miss a counted decline. + Stores guard has_shape AND has_tag oriented by the field repr + (a repr-flipping store owes a transition, so it routes generic). + `EJS_NO_SHAPE_GUARDS=1` is the compile-time bisect hook. + - **optimize-guards**: `optimizeShapeRegions` — strict linear + get-region matching, twin verification against Module.shapes + (fast slot_load ↔ slow get_prop_atom, atom==field-at-slot, + receiver identity, exit args slot-for-slot), the numeric merge's + mutation mechanics, then fact-based folding (same-block-fresh + compares only — a stale earlier-block compare can be FALSE where + the fact holds, pinned by a unit attack). Consecutive gets on + one receiver become one guard + one slow path (`p.x + p.x` ⇒ 1 + guard, 2 slot_loads). Module-toplevel receivers reload their + slot per access (distinct SSA values), so merging fires inside + functions — fine: kernels are functions; revisit with slot-load + CSE if telemetry ever says otherwise. + - **Telemetry**: stats line grows `shapeSites/shapeGuards/ + shapeDeclined=reason:n,...` (additive; the diff-lane scrape + regex untouched); `--types-dump` prints a per-site census line + (`.atom @line:col: guarded shape=... slot=N | declined reason`); + EIR-opt debug line grows shape guard/region counts. + Boxed slot ACCESS only in round one, as planned — but repr stays + part of guard identity and the imms, so shapes-P5 flips only the + emitter seam + typed-flow rules. + *Gate results (2026-07-24):* matrix green (test-eir + new shape + unit tests incl. hand-built attack IR for every verifier rule and + merge refusal, lowtier, stages 0-3, `//:test-stage1-shapes-off`); + --types diff lane **0-divergent** (459 files, 458 identical, 1 N/A + = tester.js standing esprima gap; suite-wide telemetry: 13,154 + sites consulted, 809 guarded, declines unmapped 7,575 / capped + 4,287 / empty 269 / no-field 194 / poly 12 / union-repr 8 — the + suite is string-heavy by design, kernels are where guards fire); + wrong-oracle probe `types-shapeswrong1` (repr-mismatched, + extra-field, and dictionary-mode receivers cross-module) routes + slow with node-identical output, incl. under EJS_SHAPES=off and + EJS_GC_EVERY_N_ALLOC=7; **types-bench2 guarded delta: 2.1×** + (--types 3.06s vs flag-off 6.56s; vs 5.82s with every guard + failing under EJS_SHAPES=off ⇒ ~1.9× attributable to the slot + fast paths, the rest to P3 arithmetic + P3.6); telemetry additive + (the lane's scrape regex untouched). Notables found at the gate: + (1) foldProvenGuards deleting a has_tag on a const stored value + exposed the verifier/optimizer proof-mismatch fixed via + provenNumberIntrinsic; (2) the shape-intern emitter originally + reused the literal-init function and could emit past its + terminator when a shape named an atom no access ever interned — + shapes now get their own init function, called right after + literal init. +- [x] **shapes-P4 — Born with their shape.** DONE 2026-07-24. + PRECONDITION FIRST: the differential harness grew its shapes lane + (maam submodule @d8610d3) — (a) per-allocation-site shape + containment in the analysis worker (every concrete hidden class + needs an abstract witness at its site: ⊤, or same field-name set + with pointwise ⊒ field types; order-insensitive interning on both + sides makes write order a non-issue; 350 witness checks across 2 + abstract configs, 0 violations), and (b) `shapes-obs-*.js` + observable probes run node+ejs ONLY (maam models `delete` as a + no-op and doesn't model Object.keys/freeze/defineProperty): + Object.keys order, `in` during construction, delete-then-readd, + freeze/seal, accessor conversion — each compiled BOTH default and + `--types`, both byte-matching node. Gated + vacuous-pass-guarded. + IMPLEMENTATION (design settled here, deviating from the sketch + above where the runtime's construct path forced it): + - **Literals**: statically-keyed literals lower to + `make_object_shaped` (operands = values in key order, imms.shape + = the interned ordered field list; static reprs from + operandIsNumber). Computed keys, accessors, `__proto__:`, + duplicate keys, index-looking keys, and >cap field counts keep + today's lowering. + - **Constructors are a body-side FILL, not an allocation**: the + runtime's construct path allocates `this` before the body runs, + so the batched prefix lowers to a diamond guarded by + `has_shape(this, "")` — the EMPTY shape (one compare; interning + zero fields now returns EJS_SHAPE_ROOT) — whose fast arm is + `fill_object_shaped [this, values...]` and whose slow arm is the + original sequential set_prop_atom run. The guard makes + correctness oracle-INDEPENDENT (no maam constructor query is + needed at all — constructorReportOfNode never got built); + monomorphism affects only speed. The structural fence + (oracle-free, unit-pinned): plain non-arrow function, prefix = + maximal leading run of `this. = ` statements (effect-free values ⇒ nothing can + observe the receiver mid-batch), distinct non-index names, count + in [2, cap]. `in` mid-prefix, call-valued stores, escaping + receivers, computed keys all CUT the prefix (fence_declined + counted by reason). + - **The runtime re-derives the true shape from the ACTUAL values** + (`_ejs_object_new_shaped` / `_ejs_object_fill_shaped` in + ejs-object.c take argc + names[] + values[] and walk the + transition memo, ~one compare per field when monomorphic) — a + wrong static repr can never mint a lying shape. Off-script + cases fall back to today's sequential `_ejs_object_setprop` + loop byte-for-byte: EJS_SHAPES=off, non-empty/dictionary/ + non-extensible receivers, index keys, cap — and + `shaped_proto_intercepts`: a proto-chain ACCESSOR or + non-writable data property must run assignment ([[Set]]) + semantics, so the batch declines (shaped-mode protos can't + carry either, so only dictionary-mode protos probe their maps). + - **Verifier**: operand count == shape field count (+receiver for + fill); fill requires an un-killed EMPTY-shape fact on its + receiver through the same computeShapeFacts engine as slot ops + (attack IR pins: unguarded, killed-fact, wrong-shape guard, + wrong arity). The optimizer's region/fold machinery structurally + ignores the new ops (WRITE effects fail its purity screens). + - `EJS_NO_BORN_SHAPED` is the bisect hook; telemetry: + `bornShaped=N ctorFills=N fenceDeclined=reason:n,...` + (additive). + FOUND AT THE GATE: a pre-existing shapes-P3 proof-strength mismatch — + optimize-guards' provenNumberAt proves const-number JOINS + (`c ? 1 : 0`) and folds the has_tag over one, but the verifier's + provenNumberIntrinsic didn't accept blockparams, so the uncovered + slot_store rejected a VALID optimized module (compile failure, not + a miscompile; exposed by types-bornshapewrong1's ternary-valued + ctor store, pinned by born-verify unit tests both directions). + provenNumberIntrinsic now mirrors the blockparam case. + *Gate results (2026-07-24):* harness shapes lane green (see + above) incl. the `in`-during-construction probe under `--types`; + probes types-bornshape1 / types-bornshapewrong1 node-identical + (the latter exercises guard-fail reuse, frozen receivers, + proto-setter interception, non-writable proto swallowing — + `bornShaped=3 ctorFills=3 fenceDeclined=short-prefix:1`); full + matrix ×7 green; --types diff lane 0-divergent (459 files, 458 + identical, 1 N/A tester.js; suite-wide **bornShaped=417 + ctorFills=9**, fence declines all short-prefix/value-not-local — + visible); **types-bench2 3.06s → 2.03s** (--types, median of 3; + flag-off 6.76s ⇒ **3.3×** total, the new 1.5× step being the + allocation batching: `ctorFills=1` covers the ctor in both the + kern and alloc loops). +- [x] **shapes-P5 — Typed slots × specialization × GC (compiler half).** + DONE 2026-07-24. The gc-P5 half (trace bitmaps, inline slots, + memcpy evacuation, barrier/trace elision) stays sequenced behind + the mover per gc-plan; the compiler contract it needs was finished + here. As built: + - **The seam flip** (the shapes-P3 plan, executed): `slot_load + repr:"f64"` produces a RAW f64 (lowering stamps `Inst.type`, + boxes once at the fast exit — the join stays boxed since its slow + edge is the generic get); `slot_store repr:"f64"` consumes a raw + f64 (lowering unboxes under the existing has_tag guard). The + emitter loads/stores the slot as a machine double — same address, + same 8 bytes (the NaN-box stores doubles raw), so the flip is + pure type-flow, zero runtime change. slot ops are typed by their + repr immediate the way call_typed is typed by its callee (a + per-op sig can't express either) — the verifier checks the + result stamp against the repr and requires an f64-typed operand + for f64 stores. **The typed store dissolves shapes-P3's + proof-strength hazard class**: the store's repr proof is now the + operand TYPE, which no guard-folding can strip — + provenNumberIntrinsic (the shapes-P4 escape hatch that mirrored + optimizer folds) is deleted; boxed-repr stores keep the + has_tag=false dominance rule. No off switch for the seam: it is + a contract change the verifier owns. + - **Fusion** (`shape facts feeding the raw-value machinery`): the + shape-region machinery generalizes to MIXED regions — the slow + chain admits the numeric whitelist ops, the twin check pairs + loads↔gets AND f64-ops↔generic-ops (a box_f64 of an f64 + slot_load corresponds to the load's paired get: doubles are + stored raw, so the get returns bit-for-bit the boxed rendition), + and `tryMergeShapeNumericAt` merges the NUMERIC region at a + shape region's join into it (the heterogeneous merge). After a + het merge r2's has_tag params are fed only by fast-side box_f64 + values, so foldProvenGuards (now run inside the shape fixpoint) + deletes them, rawJoinParams turns the joins raw, and the next + round's matcher grows the region — the cascade ends at ONE + has_shape guard, raw loads, raw arithmetic, one generic slow + path (`p.x*p.x + p.y*p.y` ⇒ 1 guard, 4 raw loads, 0 has_tag — + pinned at unit level). Re-executing r1's slow chain may now + re-run generic arithmetic: sound when each operand is + proven-number at the fast exit OR is one of r1's own paired gets + naming an f64-REPR field (an f64 slot holds a number by the + shaped-world invariant; the boxed-field version of that attack + is unit-pinned to refuse). `EJS_NO_SHAPE_FUSION` is the bisect + hook (criterion 6). + - **Clones**: typed slots reach P3.6 clone interiors through the + existing machinery with no new code — clone bodies lower against + `box_f64(formal)`, so the typed store's `unbox(box(p))` + annihilates into a raw store and slot loads are raw everywhere. + Clone-internal UNGUARDED slot access (dropping has_shape via the + escape fence) is NOT built: criterion 3 says later-measured- + never-first, and the measurements below show the guarded typed + path already at parity with the trusted clone — there is + currently nothing for unguardedness to win. Revisit only on + benchmark evidence (shapes-P6 discipline). + - **Telemetry**: stats line grows `shapeTyped=loads:N,stores:M` + (additive); EIR-opt debug line grows the het-merge count. + *Gate results (2026-07-24):* matrix ×7 green (test-eir + new + typed/fusion/re-exec attack unit tests, lowtier, stages 0-3, + `//:test-stage1-shapes-off`); --types diff lane 0-divergent (460 + files incl. the new probe); probe `types-typedslots1` (fused + kernel on matching + repr-mismatched + extra-field + dictionary + receivers; -0/NaN/Infinity bit-survival through raw slot traffic; + repr-flip transition mid-kernel; boxed-field stores) node-identical + in all modes incl. EJS_SHAPES=off and EJS_GC_EVERY_N_ALLOC=7. + **Measured honestly**: types-bench2 total is UNCHANGED (2.04s vs + shapes-P4's 2.03s) because 1.71s of it is the allocation loop — the + gc-P5 half owns that. The kernel itself: a variable-receiver + 20M-iteration kernel runs 0.31s under --types vs 3.28s flag-off + (10.6×), IDENTICAL between shapes-P4-boxed, shapes-P5-typed, fused, unfused, + and specialized — Apple-Silicon OoO + LLVM already hid the boxed + round-trips, so the typed/fusion wall-time delta on this hardware + is ~0. What the seam DOES buy today: an invariant-receiver kernel + (types-bench2's literal `kern(new Point(3,4), 1e6)` shape) now + CONSTANT-FOLDS COMPLETELY (0.31s → 0.00s; the boxed form never + could — LLVM can finally see the loads are pure doubles), the + guarded path reaches parity with the trusted P3.6 clone, and the + IR meets gc-P5 with one addressing seam, slot-index immediates, + and straight-line raw regions to point inline-slot addressing at. +- [x] **shapes-P6 — Measured extensions.** DONE 2026-07-24. The phase ran + as its own discipline dictates: an evidence probe per candidate + FIRST, implementation only where the numbers and a sound design + both existed. Verdicts: + - **2-way polymorphic guards: LANDED.** The evidence probe (two + Point classes {x,y} / {z,x,y} alternating through one kernel + site) first exposed a maam precision bug: `receiverShapesOfNode` + ran the `terminalShapes` subsumption filter over the JOINED + shape list, so one class's terminal ({x,y}) was absorbed by + another class's superset ({x,y,z}) exactly as if it were a + construction intermediate — 2-shape sites reported as + MONOMORPHIC on the bigger shape (sound only because the runtime + guard made the {x,y} half run generic; the suite's + "polymorphic 12" decline census was a large undercount). Fix + in maam (`analysis.ts` + pinned test): terminal-filter PER + OBJECT ADDRESS, then union — an object's own intermediates are + still subsumed, distinct classes both survive. Compiler side: + `ShapeQuery` carries 1-2 exact shapes (>2 declines + "polymorphic"; every shape must pass the full exactness screen + AND carry the accessed field — criterion 2, no near-misses; + structural duplicates dedupe to mono), and propGet/propSet + lower a guard CHAIN — the second has_shape tests on the first's + miss edge, so each fast arm sits under its own same-block-fresh + fact and the verifier's shapes-P3/shapes-P5 rules apply per arm unchanged + (typed f64 arms box at their own exits; stores split has_tag + per arm, oriented by that arm's field repr). The mono path + emits byte-identical IR to shapes-P5. The optimizer's region/fold + machinery is mono-strict and refuses chains wholesale (pinned: + 4 guards survive `p.x + p.x` un-merged, module re-verifies) — + chain-aware merging is future measured work, and wall time + says it can wait. `EJS_NO_POLY_SHAPE_GUARDS=1` is the bisect + hook (2-shape sites decline "polymorphic" exactly as before); + telemetry grows `shapePolyGuards=N` (additive). **Measured** + (M-series, types-bench3 = the bench2 kernel with alternating + receivers): chain **0.31s — parity with the monomorphic twin + (0.32s)** — vs 1.67s declined (the bisect flag) and 3.64s + flag-off: **5.4×** for the chain over the decline, and the + pre-shapes-P6 false-mono world's 0.99s (half the receivers missing + the guard) is beaten 3.2×. Probe types-poly1 (both arms fast, + typed stores per arm; cross-module repr-mismatched / third- + shape / dictionary receivers all through the shared slow path) + is identical across --types/flag-off/EJS_SHAPES=off/gc-stress. + - **Accessor inlining: DECLINED, evidence recorded.** The probe + (defineProperty proto getter, 20M dispatches — getter LITERALS + are still a maam NormalizeError) measures 2.31s under --types + vs 5.44s flag-off; the same arithmetic through shapes-P3 guarded + slots runs 0.32s, so ~7× headroom exists. But a receiver + has_shape proves NOTHING about the proto that carries the + getter (accessor-bearing protos are dictionary-mode by shapes-P2 + design — mutable maps), so sound inlining needs proto-identity + /proto-shape guard machinery plus maam-side accessor modeling + that does not exist. That is new soundness surface, not a + measured extension; revisit as its own designed phase. + - **Pretenuring hooks: DEFERRED — no consumer.** The + generational mover (gc-P2+) is not built; there is no nursery/ + tenured split for an oracle hint to steer. gc-plan owns it. + - **Array element shapes: DEFERRED, evidence recorded.** The + element-kernel probe (64-element dense f64 array, 20M reads): + 0.57s under --types vs 1.38s flag-off vs node 0.06s. Real + headroom, but arrays are exotics outside shaped mode by scope + (P4.x is plain objects), maam smashes element types, and typed + element storage is its own runtime subsystem — routed to a + future phase alongside the gc-plan storage work. + *Gate results (2026-07-24):* matrix ×7 green (test-eir + 7 new + poly unit tests incl. the optimizer-refusal pin, lowtier, stages + 0-3, `//:test-stage1-shapes-off`); --types diff lane + **0-divergent** (476 files, 475 identical, 1 N/A = tester.js; + suite telemetry: 13,323 sites, 865 guarded of which + **shapePolyGuards=25** — poly chains fire in real suite files + (eir-syntax4, shapes-storm1), not just the probes; declines: + unmapped 7,705 / capped 4,263 / empty 272 / no-field 199 / + union-repr 16 / polymorphic **3** — down from 12: the survivors + are genuine >2-shape sites, and the old count was an undercount + built on the false-mono maam reports). types-bench2 (mono world) + regression-checked bit-identical stats/output/wall-time. + +shapes-P1/shapes-P2 are pure runtime and can proceed independently of maam; shapes-P3+ +are compiler phases in the P3 mold. gc-P1 and shapes-P1 share one atomic +layout change whichever lands first. + +## Risks, named + +- **Dual-bookkeeping overhead (shapes-P1)** on shape-oblivious programs: one + transition-cache hit per property add, on every program. Measured at + the shapes-P1 gate with a hard <5% bar; the mitigation is that the + transition cache is one hash hit against an interned table vs the + map's existing hash+chain work, and shapes-P2 deletes the duplication. +- **Shape explosion from type-aware transitions.** maam's answer (caps + → megamorphic ⊤) transplants: per-object transition caps → dictionary, + global table growth monitored; splay is the canary. Order-sensitive + runtime shapes intern more than maam's order-insensitive ones — the + order-canonicalization trick is NOT available at runtime (enumeration + order is semantics); the census (shapes-P1 gate) tells us the real fanout + before any compiler work depends on it. +- **The effect-kill soundness class (shapes-P3).** Shape facts die at + WRITE|CALL effects; a missed kill is a silent miscompile of exactly the + kind P3.4's adversarial review kept finding. It gets the same + treatment: a written soundness inventory in optimize-guards, hand-built + attack IR in the unit tests, and the review loop before promotion. +- **Semantic fidelity of shaped mode.** Enumeration order, `in` during + construction, delete-readd patterns, freeze/seal, accessor conversion + — each has a dictionary-migration answer, and each needs a probe. The + runtime differential mode (EJS_SHAPES=off) is the backstop that turns + any miss into a visible diff instead of a shipped bug. +- **Cross-module shape identity** rests on module-init interning + (atom-table precedent). A module compiled against different oracle + facts than its neighbor still agrees on runtime shapes (they're + interned by structure, not by compile-time claim) — guards just fail + more often; correctness is untouched. The IR-in-manifest future + (cross-module oracle facts) only widens what qualifies. +- **Header layout coupling with gc-P1.** One layout, one atomic change, + both plans reviewed against it (gc-plan.md:317-320 owns the rule; this + doc's Step A is written to it). + +## Alternatives considered + +- **Structure-only shapes (V8-classic), representation checked per + access.** Cheaper transitions, but every typed load keeps a + `has_tag`+unbox and every guard proves less; maam already pays for + type-aware classes and P3 built the raw-f64 world this feeds. The + premium of type-aware transitions is measured at shapes-P1 (census) before + shapes-P3 commits — if type-flip churn is pathological in real code, reprs + can degrade to `boxed` per-field without changing the design. +- **Inline caches / PICs without static shapes.** A JIT's answer; AOT + echojs has no code patching and DOES have an oracle. Module-init- + interned guard globals ARE the static IC. Runtime-fed feedback could + come later via manifests; not this phase. +- **Per-class C structs from `layouts()` (full monomorphization, no + guards).** The seductive shortcut — and exactly the unguarded leap + the P3 ladder exists to prevent. Everything unguarded here rides + behind fences and the harness, or doesn't ship. +- **Deprecation/migration (V8's in-place repr rewrites).** Requires + patching compiled offsets; AOT has no second chance — this is why + reprs are in the class identity, per maam's own design note. + +## Open questions (tracked, not blocking shapes-P1/shapes-P2) + +1. **Ordered-shape witnesses from maam for constructors.** RESOLVED at + shapes-P3: maam's ShapeTable records each class's first-interning + insertion order (`fieldOrderOfShape`) — first-write program order + along the first analyzed path, for literals AND constructors alike. + A runtime object built in a different order interns a different + runtime shape and simply misses the guard (slow path, never wrong). + shapes-P4's born-with-shape constructors may still prefer the fence's + straight-line store prefix as the witness; decide there. +2. **Slot-array growth policy** (size classes vs exact + + copy-on-transition) — informed by the shapes-P1 census. +3. **How much of `Array`/`Function`/module exotics join shaped mode + later** — out of scope for P4.x entirely; plain objects first. +4. **`repr` lattice granularity** (`f64`/`boxed` vs finer `bool`/`str` + tags) — start minimal; the census + types-bench2 decide. + +## Coordination + +- **gc-plan.md**: Phase 1 header bits (joint, atomic), Phase 2 inline + allocation (born-shaped literals become bump-alloc clients), Phase 5 + (consumes shapes for tracing/evacuation; this doc's Step B). +- **maam-plan.md**: P4 checklist ticks "design doc" with this document; + shapes-P1+ items live HERE (this doc is the phase's checklist owner, the + gc-plan pattern). The differential-harness shapes lane extends the + P3.5 asset in the maam repo. +- **plans.md escape analysis / allocation sinking**: sinking deletes + allocations shapes would otherwise accelerate — run the shapes-P1 census + with the optimizer ON (the gc-P0 lesson). + +## Phase checklist (for /goal sessions) + +- [x] **shapes-P1** runtime shape table + tracking, dual bookkeeping, header + bits (joint with gc-P1), EJS_SHAPES=off, census instrumentation. + Gate: matrix ×3, off-mode diff, <5% insert overhead, census + recorded. DONE 2026-07-23 — see the phased-plan entry above for + the numbers (2.1% insert overhead via the inlined transition + memo). +- [x] **shapes-P2** slot storage + dictionary migration, specops mode-switch. + Gate: both-modes byte-identical suite+kangax, stress green, + microbench recorded. DONE 2026-07-24 — see the phased-plan entry + above (set 3.2×, get 1.09×, insert -3%; storm probe + gc-stress + green both modes; no in-repo kangax, suite+probe stand in; NOTE + the stage2 GC lesson recorded there: shaped field cap 14 keeps + slot arrays out of the LOS, and the gc trigger now scales with + heap footprint). +- [x] **shapes-P3** EIR ops + verifier inventory + emitter + maam + node-identity queries + guarded diamonds + shape facts in + optimize-guards. Gate: matrix, lane 0-divergent, wrong-oracle + probes, unit tests, types-bench2 delta. DONE 2026-07-24 — see the + phased-plan entry above (types-bench2 2.1×, lane 459 files + 0-divergent, all attack IR pinned at unit level). +- [x] **shapes-P4** born-with-shape (literals unconditional; constructors + fenced). HARD PRECONDITION: harness shapes lane. Gate: harness + + lane + probes + delta. DONE 2026-07-24 — see the phased-plan + entry (harness shapes lane green, types-bench2 3.06s → 2.03s, + ctor batching = the empty-shape-guarded body-side fill; no maam + constructor query needed). +- [x] **shapes-P5** typed slots × clones × gc-P5 consumption (compiler half; + gc-P5 consumption waits on the mover). Gate: typed delta measured + and recorded, all lanes green. DONE 2026-07-24 — see the + phased-plan entry above (raw f64 slot ops + heterogeneous region + fusion; bench2 total unchanged at 2.04s because the residual is + the alloc loop; invariant-receiver kernels now constant-fold; + guarded path at parity with trusted clones). +- [x] **shapes-P6** measured extensions — evidence-gated, all four candidates + probed and measured. DONE 2026-07-24: 2-way poly guard chains + LANDED (kernel 5.4× vs decline, mono parity; required the maam + per-object terminal-filter fix — the false-mono finding); accessor + inlining declined (7× headroom recorded, blocked on proto-guard + soundness machinery); pretenuring deferred (no mover yet — gc-plan + owns it); array element shapes deferred (numbers recorded; arrays + are outside shaped mode by scope). See the phased-plan entry. diff --git a/docs/sinking-plan.md b/docs/sinking-plan.md new file mode 100644 index 00000000..5071fc41 --- /dev/null +++ b/docs/sinking-plan.md @@ -0,0 +1,543 @@ +# sinking-plan: escape analysis + allocation sinking + +Bucket plan; the ordering spine lives in `docs/plans.md`. Phase ids +here are `sinking-P#` (formerly S1/S2/S3 in this doc's first +revision). + +Status: sinking-P1 LANDED (2026-07-25), sinking-P2 LANDED (2026-07-25), +sinking-P3 LANDED (2026-07-25) — see the results sections at the +bottom. Owner doc for extending +escape analysis + allocation sinking (docs/plans.md, optimization +phase, first bullet) past what already exists. Written 2026-07-25, +after gc-P2. + +## Where we actually are + +The plans.md ladder is further along than its checkbox suggests: + +- **Rung 1 (`make_env`)** — landed. `scalarReplaceEnvs` + (lib/eir/optimize.ts) scalar-replaces non-escaping closure + environments; the EIR inliner (`inlineDirectCalls`) exposes IIFE envs + to it. +- **Rung 2 (`make_object`/`make_array` + own-key folding)** — landed + *for the unshaped ops*. `sinkAlloc` (optimize.ts:212) folds own-key + `get_prop_atom` / const-index `get_prop` / `.length` reads to the + allocation's operands and deletes write-only allocations. +- **Rung 3 (iterator-wrapper peephole)** — landed + (`foldIteratorWrappers`): dense-array destructuring walks fold to + direct element reads. +- **Rung 4 (`rest_args`/`args_obj`)** — not started (unchanged). + +What broke the ladder: **shapes**. Under `--types`, every +statically-keyed literal lowers to `make_object_shaped` (P4.4 +born-with-shape), and every oracle-typed property read lowers to a +`has_shape` diamond (`slot_load` fast arm, `get_prop_atom` slow arm). +`sinkAlloc` matches neither op, so in exactly the compiles where +performance matters, rung 2 no longer fires. Constructor results +(`construct` of a born-shaped ctor) were never covered by any rung. + +Measured stake (types-bench2, 2026-07-25, nursery default-on): 0.64 s +vs node's 0.06 s warm. The `alloc()` loop allocates 4M Points × (1 +wrapper object + 1 slot-array env) plus fill and guard dispatch, all of +it provably dead — node deletes the allocation outright via escape +analysis + scalar replacement. This phase rebuilds that ability for +the shaped world. + +## Design + +### sinking-P1 — shaped-literal sinking (statically sound) + +Extend `sinkAllocations` to `make_object_shaped` candidates. A shaped +allocation's shape is an immediate (`imms.shape` keyed into +`Module.shapes`) and its operands are the field values in shape order, +boxed — there are no separate initializing stores. Use classification +(fail-closed, mirroring `classifyUses`): + +- `has_shape(o, S)` whose **only** consumer is its block's `cond_br` — + a guard, resolvable statically (below); +- `slot_load(o, S=alloc shape, slot=k)` in base position — own read; +- `get_prop_atom(o, atom)` in base position — own read iff `atom` + names a shape field, else a prototype read (unfoldable, blocks + removal, same as unshaped); +- **anything else escapes** — including every write (`slot_store`, + `set_prop_atom`), edge args, call/return/throw operands, value + positions, `get_prop` computed reads (v1 keeps writes out entirely; + the unshaped pass's flow-insensitive written-atom skip doesn't carry + over because a write would also invalidate guard folding). + +**Guard resolution.** For a non-escaping, never-written shaped +allocation the birth shape is invariant for the object's whole +lifetime — nothing else can transition it, so the verifier's WRITE|CALL +kill inventory (which models *other* code mutating the receiver) does +not apply. `has_shape(o, S)`: + +- `S ≠ birth shape` → fold false (branch to the false edge). +- `S = birth shape` → fold **true only if every f64-repr field's + operand is provably a number** (a `box_f64` or a number `const`); + otherwise fold **false**. Both directions are sound: the fast and + slow arms of a shape diamond are twins computing the same value, so + routing to the generic arm never changes semantics — and the folded + reads collapse to the same operand either way. The repr condition + exists because folding true exposes `slot_load repr=f64`, whose + result we fold to the *raw* source of the operand's `box_f64`; + feeding that from a non-number would manufacture garbage bits. (The + runtime enforces the same invariant dynamically: `fill/make_shaped` + re-derive the true shape from actual values, so a lying-repr operand + makes the runtime object's shape differ from the static key — the + fold-false route is the static mirror of that re-derivation.) + +Folding a guard = `condBrToBr` + drop the now-unused `has_shape` +(pure); `sweepUnreachableBlocks` reclaims the dead arm. Both helpers +already exist in optimize-guards.ts. + +**Read folding.** `slot_load slot=k repr=f64` → the operand of the +field value's `box_f64` (raw f64, type-preserving — verifier needs no +change); `repr=boxed` → the operand itself. `get_prop_atom` for field +`name` → the operand (boxed, type-preserving). Removal: when no uses +remain, delete the alloc; `removableWhenDead` gains the shaped ops +next to the existing `make_object`/`make_array` own-storage exemption. + +**Semantics note (define vs set).** Sinking a literal assumes its +field initialization is unobservable. Literal keys are define- +semantics per ES; the current runtime's shaped fallback uses setprop- +on-fresh, equivalent for every key the shaped lowering admits +(`__proto__` and computed keys are already excluded). This is the +same judgment the existing `make_object` sinking made; the +differential lane arbitrates. + +**Pass placement.** Inside the existing main fixpoint (round-robin +with inlining/env-replacement), i.e. *before* `optimizeShapeRegions` — +sinking sees per-read diamonds, never merged regions. Clones from +P3.6 specialization get their shot in the post-specialize +`optimizeModule` round. Bisect: `EJS_NO_SHAPED_SINK` (the +`EJS_NO_EIR_OPT` mold). Telemetry: `shape_allocs_sunk` + +`shape_guards_sunk` on the `EIR-opt:` line. + +### sinking-P2 — constructor-result sinking (needs a runtime contract; NOT static) + +The bench2 alloc loop is `new Point(i, i+1)` — a `construct` of a +module-local born-shaped ctor. The tempting rewrite (virtualize the +result: field k = argument k, delete the construct) is **unsound as a +static transform**, and the reason deserves recording: + +> Constructor body stores are `[[Set]]` semantics. A setter installed +> on `Point.prototype` — reachable from *any* escaped instance via +> `Object.getPrototypeOf` — must intercept `this.x = x` in every later +> construction. Deleting the store deletes the interception. This is +> exactly why P4.4's born-with-shape kept the stores and guarded the +> batched fill with a runtime `shaped_proto_intercepts` check rather +> than eliding anything. Object literals don't have this problem +> (define semantics), which is why sinking-P1 is static and sinking-P2 is not. + +Sound path (designed here, sequenced after sinking-P1): **epoch-guarded +sinking** — the deopt-free analogue of V8's speculative escape +analysis. The runtime maintains a global accessor epoch +(`_ejs_accessor_epoch`, bumped whenever an accessor property is +installed on any object — defineProperty/defineProperties/ +`__defineGetter__`/`__defineSetter__`/class accessor evaluation — and +on `setPrototypeOf`/`__proto__` writes). A sunk construct site +compiles to: + + %e = epoch_check epoch= ; load+cmp + cond_br %e -> virtual arm (no allocation, fields = args), + slow arm (the original construct) + +The guard is one load + compare against the epoch observed at module +init; the sunk arm saves two allocations, the fill, and the field-read +dispatch. Accessor installation is rare in the corpus (P4.1 census: +builtin-init dominated) but *not absent* — the epoch must be sampled +after builtin/module init, or kept per-shape-lineage. Additional sinking-P2 +conditions, all fail-closed: + +- ctor resolves through the P3.6 promoted-`%self`-slot machinery to a + module-local `make_closure` whose function passes the P4.4 fence + *and* whose body is exactly the guarded fill + `return undefined` + (any trailing code declines); +- fill operands are exactly the formals, in order (computed field + values would require real inlining — decline in v1); +- construct-site argument count equals formal count (missing-argument + `undefined` would change the runtime-derived shape); +- result non-escaping under the sinking-P1 classifier; +- all-or-nothing per site: partial folding with a surviving construct + is unsound (the surviving execution may be intercepted, diverging + from folded reads). + +sinking-P2 touches runtime (epoch maintenance), lowering (epoch_check op or a +call_runtime), and the optimizer; it is its own gated step with its +own differential evidence. Until then `new`-heavy loops keep their +allocations — gc-P2's nursery makes that a bump-pointer + minor-GC +cost rather than a free-list cost, which is the composition the two +plans always intended. + +### sinking-P3 — flow-sensitive writes, partial escapes, args (P5.3) + +Design written 2026-07-25, scoped by two investigations recorded here +so the judgments survive: + +**(a) Flow-sensitive field writes** (`lib/eir/sink-flow.ts`). Lifts +the "any write declines" rule for `make_object` and +`make_object_shaped` candidates (arrays keep the length-write decline; +element writes can't reach a literal anyway). Two structural facts +make this cheap: + +- *Write diamonds are twins.* `propSet` lowers `o.f = v` to a + has_shape diamond whose fast arm slot_stores (a possibly-unboxed) `v` + and whose slow arm set_prop_atoms the same `v` — both arms store the + same source value, so the after-join tracked value is just `v`. + Field phis are needed only at REAL control joins (if/else writing + different values, loop headers), never per diamond. +- *Folding a shape guard FALSE is unconditionally sound* (the P1 twin + argument), independent of writes. A written candidate folds every + foldable guard false and resolves everything through the generic + arms; the memory ops then vanish entirely, so nothing is lost by + skipping the typed arms — the post-fixpoint rawJoin/guard-region + passes recover raw f64 flow on the *values* (which is where the + arithmetic lives once the object is gone). This avoids the + optimistic repr-invariance simulation folding TRUE would require + under writes (a set_prop_atom storing a non-number into an f64 field + repr-transitions the runtime shape). + +The pass is all-or-nothing per candidate (the ctor-sink discipline): +every use must be a foldable target-less read (own-key +get_prop_atom / const-index get_prop on objects), a deletable +target-less own-key write (set_prop_atom naming a literal key / shape +field — [[Set]] to an own writable data property on an unaliased +object is unobservable, the P1 semantics-note judgment; *non-own-key +writes decline*: a key-adding [[Set]] walks the prototype chain and is +only epoch-guardable, recorded below), a foldable guard, or (mode b) +the single escape. slot_stores are classified as pending writes in +round 1; guard fold-false unreaches them and the sweep removes them +before flow resolution — one surviving to the resolution round +(hand-built IR only) declines. Reaching values are computed per field +with a Braun-style renamer over the complete CFG (the builder's +algorithm, minus lazy sealing), minting boxed block params at joins; +plan-before-apply screens decline candidates whose walk region touches +catch blocks (unwind edges never carry the tracked value). A +slot_store's tracked value strips the store's `unbox_f64` (sound: the +diamond's has_tag proved numberness on that arm, so box(unbox(v)) is +v); reads fold to the reaching value at their program point. Bisect: +`EJS_NO_FLOW_SINK`. Telemetry: `flow_allocs_sunk` on the `EIR-opt:` +line. + +**(b) Partial escapes / materialization.** The same pass, one escape +allowed: a candidate whose non-read/write/guard uses are exactly ONE +instruction E materializes the object immediately before E (a fresh +`make_object`/`make_object_shaped` of the reaching field values — +the runtime re-derives the true shape from actual values, so +tracked-write repr drift is immaterial) and substitutes it into E's +operands/edge-args. Fail-closed screens, each with a recorded reason: + +- *No use reachable from E* (forward CFG walk from after-E, treating + entry into the alloc's block as a fresh-activation barrier): a read + after the escape would miss external mutations through the alias. +- *The same walk finding E again declines* (at-most-once per + activation): two materializations of one abstract object would split + its identity. +- At least one read folded or write deleted (else the rewrite is + churn — `return {…}` directly is already optimal). +- Own-key writes only, exactly as in (a). + +Identity/typeof/=== against the materialized object are correct by +construction: it IS the object, created at its last-possible point. + +**(c) `rest_args`/`args_obj` — length folds land; index folds +DECLINED.** Evidence from the runtime (2026-07-25): + +- `_ejs_arguments_new` COPIES argv (ejs-arguments.c:62) and is + unmapped; `.length` is synthesized from argc on every get; + callee/caller are poison accessors. `_ejs_array_new_copy` copies. + So `.length` of either object is exactly a function of the immutable + argc — foldable to a new `arg_len` op (imms.index; boxed + `max(argc - index, 0)`; effect NONE; emitted from the raw argc + calling-convention value, the rest_args precedent). +- Late argv reads WOULD be GC-safe (the conservative whole-stack scan + still covers the caller's args scratch and pins win over evacuation + — ejs-gc.c:1982-1989 — and generator bodies never see caller argv: + the desugar materializes arguments/rest in the outer function, so + they reach the body through env capture, which classifies as an + escape and declines). But an out-of-bounds `arguments[k]`/`rest[k]` + read falls through to the ordinary get path — the prototype chain — + and writable INTEGER DATA properties on Array.prototype / + Object.prototype do not bump `_ejs_accessor_epoch` + (ejs-object.c:2594's screen covers accessor/non-writable defines and + setPrototypeOf only). A sound `arg_load` therefore needs either a + new proto-index epoch class in the runtime or an epoch-guarded + region with an OOB helper (receiver-free data-prop lookup is only + sound while the accessor epoch is 0). Corpus census: const-index + arguments reads are rare and co-occur with uses that decline anyway + (iteration, aliasing tests); the recurring foldable pattern is + arity-check `.length`. Decision: implement `arg_len` only; record + `arg_load` here as declined-with-design until a workload justifies + the runtime extension. + +`arg_len` joins the inliner's and specializer's frame-op screens +(FRAME_OPS / CLONE_FRAME_OPS — it consumes the raw argc, which +neither an inlined body nor a specialized clone carries; clones can +never contain a minted arg_len since functions using arguments/rest +are never cloned, but the screens keep the invariant explicit). The +sink itself: a rest_args/args_obj whose every use is a target-less +`get_prop_atom "length"` folds those reads to `arg_len` and removes +the allocation in-pass (args_obj's THROW effect keeps it out of +generic DCE deliberately — the pass, having proven all uses folded, +removes it explicitly). Any other use — writes, computed reads, +`Symbol.iterator`, callee — declines. Fires on flag-off compiles too +(like the unshaped sink); the stage matrix is the gate. Bisect: +`EJS_NO_ARGS_SINK`; telemetry: `args_sunk`. + +**Still recorded, not scheduled** (sinking-P4 material): + +- Key-ADDING writes on sunk objects (epoch-guarded; subsumes the + `var o = {}; o.a = …` builder pattern under --types, where the + literal's birth shape lacks the written key). +- `arg_load` per the design above. +- Cross-block env scalar replacement (the same Braun machinery over + env slots; today `scalarReplaceEnvs` is same-block only) — belongs + with compiler-P1's SSA cleanups. +- Cross-function sinking via inlining heuristics beyond the current + single-block IIFE inliner (a multi-block inliner would let sinking-P2's + "fill operands are formals" restriction relax to arbitrary ctor + prefixes). + +## Gates + +sinking-P1: unit tests (fold + refusal attacks: escaping uses, written +fields, wrong-shape guards, non-number f64 operands folding false, +prototype reads blocking removal, `===` identity, typeof); the +existing suite byte-identical under `EJS_NO_SHAPED_SINK` vs default +for flag-off compiles (shaped ops only exist under --types); types +diff lane 0-divergent; matrix ×7; telemetry counts on the suite +recorded here; probe types-sink1 node-identical incl. EJS_SHAPES=off +and gc-stress. Perf: a shaped-literal kernel (sink-probe2-style) +should reduce to pure arithmetic — verify via `--dump-after eir-opt` +and wall time. + +sinking-P2 (when built): everything above plus epoch-bump coverage tests +(accessor installed mid-loop → slow arm taken from that iteration on), +and types-bench2 as the phase bench — target is the alloc() loop at +kern parity (~0.3 s total, from 0.64 s). + +sinking-P3: unit tests per feature with refusal attacks (non-own-key +write, use-after-escape, escape-in-loop-without-alloc, two escapes, +catch-block join, surviving slot_store, computed read on args, write +to rest, bisect hooks); semantic probes node-identical incl. +`EJS_SHAPES=off`, gc-stress (`EJS_GC_EVERY_N_ALLOC=101`), and +flag-compiled (`EJS_NO_FLOW_SINK` / `EJS_NO_ARGS_SINK`) exes — +probes must cover write-then-read-across-branches, loop accumulator +objects, escape-site identity (`===`, mutation through the escaped +alias), and arguments-length arity dispatch; --types diff lane +0-divergent; matrix ×7 (args/flow sinking fire flag-off, so the stage +lanes carry real weight here); a flow-sink loop-accumulator kernel as +the phase bench, A/B vs `EJS_NO_FLOW_SINK`. + +## sinking-P1 results (2026-07-25) + +Implementation: `sinkShapedAlloc` in lib/eir/optimize.ts, wired into +the existing `sinkAllocations` under the main fixpoint; guard branches +resolve via `condBrToBr` + `sweepUnreachableBlocks` (now exported from +optimize-guards.ts and swept each fixpoint round); shaped allocs join +the own-storage DCE exemption, and a shaped alloc reaching DCE counts +as the sink completing (`shape_allocs_sunk` / `shape_guards_sunk` on +the `EIR-opt:` stats line). Bisect: `EJS_NO_SHAPED_SINK`. + +The canonical reduction (types-sink2 kernel, `--dump-after eir-opt`): +`f$typed(a) { var o = {a: n, b: n+1}; return o.a + o.b }` compiles to +two `f64_add`s and a return — allocation, guards, boxes, and slot +loads all gone; the raw-join machinery (P3.4/P4.5) carries the folded +operands through the emptied diamond joins. + +Note on repr provability in practice: unit-lowered IR feeds field +values as raw params (never `box_f64`), so guards there resolve to the +generic arm — reads still fold to the same operands and the alloc +still drains; in real compiles the specialized clones box their +formals, guards resolve true, and the raw path folds. Both routes +were pinned by tests. + +## sinking-P2 results (2026-07-25) + +Implementation, in the three pieces the design called for: + +- **Runtime** (`_ejs_accessor_epoch`, ejs-object.{h,c}): one global + counter, `== 0` meaning "no user code has installed anything that + could intercept a [[Set]] through a fresh object's prototype chain". + Bumps at the ordinary `DefineOwnProperty` specop for accessor + descriptors and `writable:false` data descriptors, and at both + `SetPrototypeOf` implementations (ordinary + proxy trap); zeroed at + the end of `_ejs_init` so builtin installs never count (the only + builtin accessor on a fresh ordinary chain is `__proto__`, a name the + ctor fence never admits). **The screen that made it viable: only + defines on ORDINARY receivers bump.** A virtualized instance's chain + is `ctor.prototype → Object.prototype`, both ordinary, and any other + object can only join such a chain through a bumping setPrototypeOf or + a statically-declined prototype swap — without the screen, every + closure's non-writable name/length and every module's export + accessors killed the epoch at startup (found by lldb watchpoint on + the first bench run: `_ejs_function_new` at module init). +- **EIR** `epoch_check` op (arity 0, READ, i1): emitted as one load of + the global + compare-to-zero (`emitAccessorEpochCheck`, the + `_ejs_heap` global-seam precedent). No verifier change — the op + table's sig covers it. +- **Optimizer** (`lib/eir/sink-construct.ts`, module pass after + specialization in integrate.ts): resolves construct callees through + the promoted-`%self`-slot discipline (single closure store, + prefix-safe or store-dominated, **every load of the slot used only as + a call/construct callee — which also closes the `Point.prototype = X` + replacement hole statically**, so exotic protos need a bumping + setPrototypeOf); structurally matches the ctor body as exactly the + P4.4 guarded fill of the formals plus `return undefined`; requires + argc == formal count and the sinking-P1 use classification on the + result; computes the single-entry single-exit acyclic use region; + runs a fold simulation (the sinkShapedAlloc guard rule) proving every + use folds or dies unreachable — the all-or-nothing guarantee that the + virtual arm's allocation always drains. The rewrite splits at the + construct, closes the head with `epoch_check` + cond_br, keeps the + original region as the slow arm, and clones the region with the + construct replaced by `make_object_shaped(args)`; region-defined + values used past the exit cross through minted join params (rawJoin + for f64). The existing shaped-literal sink then drains the clone in + the post-sink optimizer round. Bisect: `EJS_NO_CTOR_SINK`; + telemetry: `ctorSunk=N` on the `--types:` line, `EIR-ctor-sink` debug + line. + +Gate evidence (all green, 2026-07-25): + +- 192 EIR unit tests (5 new `sink-ctor`: full sink, live-outs across + the epoch join, six refusal attacks in one sweep — second store / + prototype-touching load / trailing ctor code / swapped fill operands + / argc mismatch / escaping result — non-promoted slot, bisect hook). +- Probes `types-ctorsink1` (epoch coverage: clean run, accessor + installed mid-loop through Object.prototype at i=5, then a + non-writable data property mid-loop — slow arm and interception from + that iteration on) and `types-ctorsink2` (pure-win kernel + escape + decline + prototype-method decline): node-identical, including under + `EJS_SHAPES=off`, `EJS_GC_EVERY_N_ALLOC=101`, and an + `EJS_NO_CTOR_SINK` compile. +- `--types` diff lane: 493 files, 492 identical, 0 divergent, 1 N/A + (tester.js, standing). ctorSunk fires in types-bench2 (2), + types-sink1 (2), and the two new probes — everywhere else the + fail-closed screens decline. +- Matrix ×7 green (test-eir, lowtier, stages 0-3 at 419 pass / + 22 standing xfail each, shapes-off lane). +- **types-bench2: 0.70s → 0.26s wall (warm, A/B vs EJS_NO_CTOR_SINK + exes from the same tree); allocations 4,000,501 objects + 4,000,061 + envs → 501 + 61 (EJS_GC_PROFILE).** The alloc() loop is + allocation-free — better than the ~0.3s phase target; the residual + 0.26s is kern. + +What the sunk loop still pays per iteration: one epoch load+compare, +one `%self` slot load of the ctor (kept live by the slow arm), and two +generic `add` calls (the oracle doesn't type s + p.x, so those adds +never had diamonds) — all noise next to the construct it replaced. +Recorded for later phases: slot-load licm and add-diamond coverage +would shave the rest. + +## sinking-P3 results (2026-07-25) + +Implementation, per the design above: + +- **Flow pass** (`lib/eir/sink-flow.ts`): planOne (classify + all + screens, zero mutation) → applyPlan (fold guards false, sweep, + Braun-rename per field with minted boxed join params + trivial-param + removal, fold reads, materialize at the single escape, delete writes + + alloc). Runs last in the optimizeFunction fixpoint round with its + own use scan, one rewrite per invocation. Bisect: + `EJS_NO_FLOW_SINK`; telemetry `flow_allocs_sunk` / + `allocs_materialized` (guard folds count into `shape_guards_sunk`). +- **Args sinking** (`sinkArgsObjects` in optimize.ts): new `arg_len` + op (emitted as a call to the new pure `_ejs_arg_length(argc, index)` + runtime helper — node-llvm has no SIToFP binding, so the int→boxed + conversion lives in C); rest_args/args_obj whose every use is a + target-less `.length` read fold and are removed in-pass. `arg_len` + joined FRAME_OPS and CLONE_FRAME_OPS. Bisect: `EJS_NO_ARGS_SINK`; + telemetry `args_sunk`. `arg_load` declined per the design section + (OOB prototype-read hazard uncovered by the epoch; census: rare). +- Two renamer bugs found by the stage1 self-compile, both worth + remembering: (1) the trivial-param scan judged a MID-FILL param + (`[null, X]` read as all-equal-X) — unfilled slots now decline + judgment; (2) a recursion frame's captured param could be forwarded + by a nested trivial-param cascade before installation (its + replaceAllUses runs too early to see the use) — a `forwarded` map + + `resolve()` at every install point closes it. + +Self-compile cost, and what it taught (the stage2 build initially ran +~2× slow; each finding below is now in the code): + +- **Never read process.env in the fixpoint** — under the self-hosted + runtime it is a rebuild-the-environment getter. All sink bisect + flags are read once per optimizeFunction (`SinkFlags`), which also + hoisted the pre-existing per-round `EJS_NO_SHAPED_SINK` read. +- **One scan per round** — the driver's `scanRound` gathers the use + map AND every sink pass's candidate list in a single `forEachInst` + walk; the flow pass consumes the shared map (type-only imports keep + optimize↔sink-flow acyclic at runtime) and does no scans of its own. +- **FLOW_REGION_CAP (32 blocks)** — sinking spreads field values + across the rename region as live SSA values, so a function-spanning + region trades one heap object for many long-lived gc-frame slots: + flow-sinking esprima's `scanPunctuator` token literal measurably + worsened every minor GC's conservative pin scan during parses. + Small regions (loop accumulators, builder tails) keep the win; the + self-compile census after the cap is 3 sites → 0–1 per big module. +- The remaining ~1.5–2× stage-self-compile wall delta is NOT the + passes (it persists with both bisect flags set): it is a + pre-existing, mmap-layout-bistable conservative-pin-scan cliff that + any allocation-pattern change (+1.4% allocs here) can tip — fully + root-caused and recorded as gc-P4's first order of business in + gc-plan.md, with a partial mitigation (the LOS bounds prefilter, + ejs-gc.c) landed in this phase. + +Gate evidence (all green, 2026-07-25): + +- 205 EIR unit tests (new: sink-flow ×8 — cross-branch phi, loop + accumulator, read-before-write, escape materialization, five-way + refusal sweep, catch-region decline, shaped partial escape, bisect + hook; sink-args ×4 — arguments/rest length folds, four-way refusal + sweep, bisect hook; the two sinking-P1-era "writes decline" pins now + assert the flow-sunk behavior with EJS_NO_FLOW_SINK variants + pinning the old decline). +- Probes `test/types-flowsink1.js` (branches, loop accumulator, + read-before-write, escape identity + mutation-through-alias, fresh + object per loop iteration, key-adding decline, try-write decline, + self-reference decline, and an Object.prototype setter intercepting + the declined key-adding write) and `test/types-argsink1.js` + (length-only folds incl. rest start index, computed-read / + forwarding / arrow-capture / generator declines): node-identical + under --types, flag-off, `EJS_SHAPES=off`, + `EJS_GC_EVERY_N_ALLOC=101`, and `EJS_NO_FLOW_SINK` / + `EJS_NO_ARGS_SINK` compiles. Probe telemetry: 6 flow-sunk + (3 materialized) / 5 args objects sunk; every refusal case declines. +- `--types` diff lane: 474 files, 473 identical, 0 divergent, 1 N/A + (tester.js, standing). (The P2-era 493 count included stale extra + copies in the old work tree; the tracked corpus is 472 + the two new + probes.) +- Matrix ×7 green (test-eir, lowtier, stages 0-3 at 421 pass / 22 + standing xfail / 0 fail each — the 419 + the two new probes — + shapes-off lane) — stage1/2/3 self-compiles carry the flow pass + live (post-cap it fires on the compiler's own classifier-record + pattern: object literal of arrays + flag, pushed into and + returned). +- **Phase bench `test/types/types-bench4.js`** (loop-accumulator + object, read+write per iteration, plus a partial-escape twin): + **0.04 s vs 0.15 s under EJS_NO_CTOR_SINK-style A/B + (`EJS_NO_FLOW_SINK` exes from the same tree), 3.75×; node warm is + 0.20 s** — the win is the per-iteration slot/diamond memory traffic + (GC profile: 625→585 allocs, the 40 per-call accumulator objects). +- types-bench2 unchanged at 0.27 s (0.26 s landed; noise). + +Recorded for sinking-P4 (see the design section's +"still recorded" list): key-adding writes under an epoch guard, +`arg_load`, cross-block env scalarization, multi-escape +materialization (each-path-at-most-once), and forwarding single-pred +join params left behind by the fold (LLVM collapses them today; an +EIR-level cleanup would help downstream passes see through). + +Gate evidence: 187 EIR unit tests green (8 new: full sink, escape / +call-operand / write / prototype-read / wrong-shape / hand-built +unprovable-repr refusals, bisect hook); --types diff lane 485 +identical / 0 divergent / 1 N/A over 486 files (including new suite +tests types-sink1/2, node-identical); matrix ×7 green; flag-off +lowering unchanged (shaped ops only exist under --types; the +unreachable-block sweep now also prunes builder-era dead blocks in +flag-off compiles — semantically inert, LLVM dropped them anyway). +types-bench2 unchanged at 0.65 s as predicted (its allocations are the +sinking-P2 constructor case); the sinking-P1 payoff lands on non-escaping literal +patterns — destructuring returns, options objects — throughout the +suite and the compiler itself. diff --git a/ejs-es6.js b/ejs-es6.js deleted file mode 100755 index 3ce490ff..00000000 --- a/ejs-es6.js +++ /dev/null @@ -1,754 +0,0 @@ -import * as os from "@node-compat/os"; -import * as path from "@node-compat/path"; -import * as fs from "@node-compat/fs"; -import * as child_process from "@node-compat/child_process"; - -import * as debug from "./lib/debug"; -import { compile } from "./lib/compiler"; -import { dumpModules, getAllModules, gatherAllModules } from "./lib/passes/gather-imports"; - -import { bold, reset, genFreshFileName, Writer } from "./lib/echo-util"; -import { Triple } from "./lib/triple"; - -import { - LLVM_SUFFIX as DEFAULT_LLVM_SUFFIX, - RUNLOOP_IMPL as DEFAULT_RUNLOOP_IMPL, -} from "./lib/host-config"; - -let spawn = child_process.spawn; - -function isNode() { - return typeof __ejs == "undefined"; -} - -let argv; -if (!isNode()) { - // argv is ['.../ejs', ...], get rid of the first arg - argv = process.argv.slice(1); -} else { - // argv is ['node', '.../ejs-es6.js', ...], get rid of the first two args - argv = process.argv.slice(2); -} - -let ejs_dirname; -function ejs_exe_dirname() { - if (ejs_dirname) return ejs_dirname; - let argv0 = process.argv[isNode() ? 1 : 0]; - let cwd = process.cwd(); - - let full_path_to_exe; - if (argv0.indexOf("/") != -1) { - // either relative or absolute. don't both searching path. - let ejs_path = path.resolve(cwd, argv0); - try { - if (fs.statSync(ejs_path).isFile()) { - full_path_to_exe = ejs_path; - } - } catch (e) { - // an exception while stat'ing is the same as the file not existing. - } - } else { - // not qualified at all, search over PATH - for (let p of process.env.PATH.split(":")) { - let ejs_path = path.resolve(cwd, p, argv0); - try { - if (fs.statSync(ejs_path).isFile()) { - full_path_to_exe = ejs_path; - break; - } - } catch (e) { - // we treat an exception while stat'ing the same as the file not existing. - } - } - } - if (!full_path_to_exe) { - throw new Error("could not locate ejs executable"); - } - - ejs_dirname = path.dirname(full_path_to_exe); - return ejs_dirname; -} - -function relative_to_ejs_exe(n) { - let was_array = Array.isArray(n); - if (!was_array) n = [n]; - - let rv; - if (isNode()) { - rv = n.map((el) => path.resolve(ejs_exe_dirname(), "../..", el)); - } else { - rv = n.map((el) => path.resolve(ejs_exe_dirname(), el)); - } - - if (was_array) return rv; - return rv[0]; -} - -let temp_files = []; - -let host_triple = Triple.fromProcess(); -let target_triple = host_triple; // a reasonable default. we're compiling for _this_ triple. - -let options = { - // our defaults: - opt_level: 2, - debug: false, - debug_level: 0, - debug_passes: new Set(), - warn_on_undeclared: false, - frozen_global: false, - record_types: false, - output_filename: null, - show_help: false, - leave_temp_files: false, - native_module_dirs: [], - extra_clang_args: "", - ios_sdk: "9.2", - ios_min: "8.0", - osx_min: "11.0", - import_variables: [], - srcdir: false, - stdout_writer: new Writer(process.stdout), -}; - -function add_native_module_dir(dir) { - options.native_module_dirs.push(dir); -} - -function set_target(str) { - let triple; - switch(str) { - case "linux_x86_64": triple = new Triple("x86_64", "unknown", "linux"); break; - case "macos": triple = new Triple("arm64", "apple", "darwin"); break; - case "iossim": triple = new Triple("arm64", "apple", "darwin"); break; - case "iosdev": triple = new Triple("arm64", "apple", "darwin"); break; - default: - triple = Triple.fromString(str); - break; - } - target_triple = triple; -} - -function set_extra_clang_args(arginfo) { - options.extra_clang_args = arginfo; -} - -function increase_debug_level() { - options.debug_level += 1; -} - -function add_debug_after_pass(passname) { - options.debug_passes.add(passname); -} - -function add_import_variable(arg) { - let equal_idx = arg.indexOf("="); - if (equal_idx == -1) throw new Error("-I flag requires ="); - - options.import_variables.push({ - variable: arg.substring(0, equal_idx), - value: arg.substring(equal_idx + 1), - }); -} - -let args = { - "-O0": { - handler: () => (options.opt_level = 0), - help: "Optimization level 0.", - }, - "-O1": { - handler: () => (options.opt_level = 1), - help: "Optimization level 1. Similar to clang -O1", - }, - "-O2": { - handler: () => (options.opt_level = 2), - help: "Optimization level 2. Similar to clang -O2 (default)", - }, - "-O3": { - handler: () => (options.opt_level = 3), - help: "Optimization level 3. Similar to clang -O3", - }, - "-g": { - flag: "debug", - help: "enable debugging of generated code", - }, - "-q": { - flag: "quiet", - help: "don't output anything during compilation except errors.", - }, - "-I": { - handler: add_import_variable, - handlerArgc: 1, - help: "add a name=value mapping used to resolve module references.", - }, - "-d": { - handler: increase_debug_level, - handlerArgc: 0, - help: "debug output. more instances of this flag increase the amount of spew.", - }, - "--debug-after": { - handler: add_debug_after_pass, - handlerArgc: 1, - help: "dump the IR tree after the named pass", - }, - "-o": { - option: "output_filename", - help: "name of the output file.", - }, - "--leave-temp": { - flag: "leave_temp_files", - help: "leave temporary files in $TMPDIR from compilation", - }, - "--moduledir": { - handler: add_native_module_dir, - handlerArgc: 1, - help: "--module path-to-search-for-modules", - }, - "--help": { - flag: "show_help", - help: "output this help info.", - }, - "--extra-clang-args": { - handler: set_extra_clang_args, - handlerArgc: 1, - help: "extra arguments to pass to the clang command (used to compile the .s to .o)", - }, - "--record-types": { - flag: "record_types", - help: "generates an executable which records types in a format later used for optimizations.", - }, - "--frozen-global": { - flag: "frozen_global", - help: "compiler acts as if the global object is frozen after initialization, allowing for faster access.", - }, - "--warn-on-undeclared": { - flag: "warn_on_undeclared", - help: "accesses to undeclared identifiers result in warnings (and global accesses). By default they're an error.", - }, - "--target": { - handler: set_target, - handlerArgc: 1, - help: "--target linux_x86_64|macos|iossim|iosdev", - }, - "--ios-sdk": { - option: "ios_sdk", - help: "the version of the ios sdk to use. useful if more than one is installed. Default is 7.0.", - }, - "--ios-min": { - option: "ios_min", - help: "the minimum version of iOS to support. Default is 8.0.", - }, - "--osx-min": { - option: "osx_min", - help: "the minimum version of OSX to support. Default is 11.0.", - }, - "--srcdir": { - flag: "srcdir", - help: "internal flag. if set, will look for libecho/libpcre/etc from source directory locations.", - }, -}; - -function output_usage() { - console.warn("Usage:"); - console.warn(" ejs [options] file1.js file2.js file.js ..."); -} - -function output_options() { - console.warn("Options:"); - for (let a of Object.keys(args)) { - console.warn(` ${a}: ${args[a].help}`); - } -} - -let file_args; - -if (argv.length > 0) { - for (let ai = 0, ae = argv.length; ai < ae; ai++) { - if (args[argv[ai]]) { - let o = args[argv[ai]]; - if (o.flag) { - options[o.flag] = true; - } else if (o.option) { - options[o.option] = argv[++ai]; - } else if (o.handler) { - let handler_args = []; - for (let i = 0, e = o.handlerArgc; i < e; i++) handler_args.push(argv[++ai]); - o.handler.apply(null, handler_args); - } - } else { - // end of options signals the rest of the array is files - file_args = argv.slice(ai); - break; - } - } -} - -if (options.show_help) { - output_usage(); - console.warn(""); - output_options(); - process.exit(0); -} - -if (!file_args || file_args.length === 0) { - output_usage(); - process.exit(0); -} - -if (!options.quiet) { - console.log( - `host: ${host_triple}, target: ${target_triple}` - ); -} - -debug.setLevel(options.debug_level); - -let o_filenames = []; - -let compiled_modules = []; - -let sim_base = "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform"; -let dev_base = "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform"; - -let sim_bin = `${sim_base}/Developer/usr/bin`; -let dev_bin = `${dev_base}/Developer/usr/bin`; - -function target_llc_args(triple) { - let args = [`-march=${triple.llcArch()}`]; - switch (triple.os) { - case "darwin": - switch (triple.arch) { - case "arm": - args = args.concat([ - `-mtriple=thumbv7-apple-ios${options.ios_min}.0`, - "-mattr=+v6", - "--relocation-model=pic", - "-soft-float", - ]); - break; - case "arm64": - args = args.concat([ - `-mtriple=arm64-apple-macosx${options.osx_min}.0`, - "-mattr=+fp-armv8", - "--relocation-model=pic", - ]); - break; - case "x86": - args = args.concat([ - `-mtriple=i386-apple-ios${options.ios_min}.0`, - "--relocation-model=pic", - ]); - break; - case "x86_64": - args = args.concat([`-mtriple=x86_64-apple-macosx${options.osx_min}.0`]); - break; - } - break; - case "linux": - args = args.concat([ - "--relocation-model=pic" - ]) - break; - } - - return args; -} - -let target_linker = process.env.CXX || "clang++"; - -function target_link_args(triple) { - let args = ["-arch", triple.clangArch()]; - - if (triple.os === "linux") { - // on ubuntu 14.04, at least, clang spits out a warning about this flag being unused (presumably because there's no other arch) - if (triple.arch === "x86_64") return []; - return args; - } - - if (triple.os === "darwin") { - // we need more here now that everything is apple silicon - if (triple.arch === "x86_64" || triple.arch === "arm64") return args; - if (triple.arch === "x86") - return args.concat([ - "-isysroot", - `${sim_base}/Developer/SDKs/iPhoneSimulator${options.ios_sdk}.sdk`, - `-miphoneos-version-min=${options.ios_min}`, - ]); - return args.concat([ - "-isysroot", - `${dev_base}/Developer/SDKs/iPhoneOS${options.ios_sdk}.sdk`, - `-miphoneos-version-min=${options.ios_min}`, - ]); - } - - return []; -} - -function target_libraries(triple) { - if (triple.os === "linux") { - if (DEFAULT_RUNLOOP_IMPL == "noop") return ["-lunwind", "-lpthread"]; - return ["-lunwind", "-lpthread", "-luv"]; - } - - if (triple.os === "darwin") { - let rv = ["-framework", "Foundation"]; - - // for macos we only need Foundation and AppKit - if (triple.arch === "x86_64" || triple.arch === "arm64") return rv.concat(["-framework", "AppKit"]); - - // for any other darwin we're dealing with ios, so... - return rv.concat([ - "-framework", - "UIKit", - "-framework", - "GLKit", - "-framework", - "OpenGLES", - "-framework", - "CoreGraphics", - ]); - } - return []; -} - -function target_libecho(triple) { - if (options.srcdir) { - if (triple.os === "darwin") { - if (triple.arch === "x86_64" || triple.arch === "arm64") return "runtime/libecho.a"; - if (triple.arch === "x86") return "runtime/libecho.a.sim"; - if (triple.arch === "arm") return "runtime/libecho.a.armv7"; - - throw new Error("no libecho for this platform"); - } - - return "runtime/libecho.a"; - } else { - return path.join(relative_to_ejs_exe(`../lib/${triple.arch}-${triple.os}`), "libecho.a"); - } -} - -function target_extra_libs(triple) { - if (options.srcdir) { - if (triple.os === "linux") - return [ - "external-deps/double-conversion-linux/double-conversion/libdouble-conversion.a", - "external-deps/pcre-linux/.libs/libpcre16.a", - ]; - - if (triple.os === "darwin") { - if (triple.arch === "x86_64") - return [ - "external-deps/double-conversion-osx/double-conversion/libdouble-conversion.a", - "external-deps/pcre-osx/.libs/libpcre16.a", - ]; - if (triple.arch === "x86") - return [ - "external-deps/double-conversion-iossim/double-conversion/libdouble-conversion.a", - "external-deps/pcre-iossim/.libs/libpcre16.a", - ]; - if (triple.arch === "arm") - return [ - "external-deps/double-conversion-iosdev/double-conversion/libdouble-conversion.a", - "external-deps/pcre-iosdev/.libs/libpcre16.a", - ]; - if (triple.arch === "arm64") - return [ - "external-deps/double-conversion-osx/double-conversion/libdouble-conversion.a", - "external-deps/pcre-osx/.libs/libpcre16.a", - ]; - } - - throw new Error("no pcre for this platform"); - } else { - return ["libdouble-conversion.a", "libpcre16.a"].map((lib) => - path.join(relative_to_ejs_exe(`../lib/${triple.arch}-${triple.os}`), lib) - ); - } -} - -function target_path_prepend(triple) { - if (triple.os === "darwin") { - if (triple.arch === "x86") return sim_bin; - if (triple.arch === "arm64") return dev_bin; - } - return ""; -} - -let llvm_commands = {}; -for (let x of ["opt", "llc", "llvm-as"]) - llvm_commands[x] = `${x}${process.env.LLVM_SUFFIX || DEFAULT_LLVM_SUFFIX}`; - -function compileFile(filename, parse_tree, modules, files_count, cur_file, compileCallback) { - let base_filename = genFreshFileName(path.basename(filename)); - - if (!options.quiet) { - let suffix = options.debug_level > 0 ? ` -> ${base_filename}` : ""; - - // loop over import variables, replacing their values with - // their names for output - let output_name = filename; - for (let ivar of options.import_variables) { - output_name = output_name.replace(ivar.value, `$${ivar.variable}`); - } - options.stdout_writer.write( - `[${cur_file}/${files_count}] ${bold()}COMPILE${reset()} ${output_name}${suffix}` - ); - } - - let compiled_module; - try { - compiled_module = compile(parse_tree, base_filename, filename, modules, options, target_triple); - } catch (e) { - console.warn(`${e}`); - if (options.debug_level == 0) process.exit(-1); - throw e; - } - - function tmpfile(suffix) { - return `${os.tmpdir()}/${base_filename}-${target_triple.arch}-${ - target_triple.os - }${suffix}`; - } - let ll_filename = tmpfile(".ll"); - let bc_filename = tmpfile(".bc"); - let ll_opt_filename = tmpfile(".ll.opt"); - let o_filename = tmpfile(".o"); - - temp_files.push(ll_filename, bc_filename, ll_opt_filename, o_filename); - - let opt_level = options.opt_level > 0 ? `default,` : ""; - - let llvm_as_args = [`-o=${bc_filename}`, ll_filename]; - let opt_args = [ - `-passes=${opt_level}strip-dead-prototypes`, - "-S", - `-o=${ll_opt_filename}`, - bc_filename, - ]; - let llc_args = target_llc_args(target_triple).concat([ - "-filetype=obj", - `-o=${o_filename}`, - ll_opt_filename, - ]); - - debug.log(1, `writing ${ll_filename}`); - compiled_module.writeToFile(ll_filename); - debug.log(1, `done writing ${ll_filename}`); - - // debug.log (1, `writing ${bc_filename}`); - // compiled_module.writeBitcodeToFile(bc_filename); - // debug.log (1, `done writing ${bc_filename}`); - - compiled_modules.push({ - filename: options.basename ? path.basename(filename) : filename, - module_toplevel: compiled_module.toplevel_name, - }); - - if (!isNode()) { - // in ejs spawn is synchronous. - spawn(llvm_commands["llvm-as"], llvm_as_args); - spawn(llvm_commands["opt"], opt_args); - spawn(llvm_commands["llc"], llc_args); - o_filenames.push(o_filename); - compileCallback(); - } else { - let llvm_as = spawn(llvm_commands["llvm-as"], llvm_as_args); - llvm_as.stderr.on("data", (data) => console.warn(`${data}`)); - llvm_as.on("error", (err) => { - console.warn(`error executing ${llvm_commands["llvm-as"]}: ${err}`); - process.exit(-1); - }); - llvm_as.on("exit", (/* XXX code*/) => { - debug.log(1, `executing '${llvm_commands["opt"]} ${opt_args.join(" ")}'`); - let opt = spawn(llvm_commands["opt"], opt_args); - opt.stderr.on("data", (data) => console.warn(`${data}`)); - opt.on("error", (err) => { - console.warn(`error executing #{llvm_commands['opt']}: ${err}`); - process.exit(-1); - }); - opt.on("exit", (/* XXX code*/) => { - debug.log(1, `executing '${llvm_commands["llc"]} ${llc_args.join(" ")}'`); - let llc = spawn(llvm_commands["llc"], llc_args); - llc.stderr.on("data", (data) => console.warn(`${data}`)); - llc.on("error", (err) => { - console.warn(`error executing ${llvm_commands["llc"]}: ${err}`); - process.exit(-1); - }); - llc.on("exit", (/* XXX code*/) => { - o_filenames.push(o_filename); - compileCallback(); - }); - }); - }); - } -} - -function generate_import_map(js_modules, native_modules) { - let map_path = `${os.tmpdir()}/${genFreshFileName(path.basename(main_file))}-import-map.cpp`; - - let map_contents = ""; - map_contents += `#include "ejs-module.h"\n`; - map_contents += 'extern "C" {\n'; - - js_modules.forEach((module) => { - map_contents += `extern EJSModule ${module.module_name};\n`; - map_contents += `extern ejsval ${module.toplevel_function_name} (ejsval env, ejsval _this, uint32_t argc, ejsval *args);\n`; - }); - - map_contents += "EJSModule* _ejs_modules[] = {\n"; - js_modules.forEach((module) => { - map_contents += ` &${module.module_name},\n`; - }); - map_contents += "};\n"; - - map_contents += "ejsval (*_ejs_module_toplevels[])(ejsval, ejsval, uint32_t, ejsval*) = {\n"; - js_modules.forEach((module) => { - map_contents += ` ${module.toplevel_function_name},\n`; - }); - map_contents += "};\n"; - map_contents += "int _ejs_num_modules = sizeof(_ejs_modules) / sizeof(_ejs_modules[0]);\n\n"; - - native_modules.forEach((module) => { - map_contents += `extern ejsval ${module.init_function} (ejsval exports);\n`; - }); - - map_contents += "EJSExternalModule _ejs_external_modules[] = {\n"; - native_modules.forEach((module) => { - map_contents += ` { "@${module.module_name}", ${module.init_function}, 0 },\n`; - }); - map_contents += "};\n"; - map_contents += - "int _ejs_num_external_modules = sizeof(_ejs_external_modules) / sizeof(_ejs_external_modules[0]);\n"; - - let entry_module = file_args[0]; - if (entry_module.lastIndexOf(".js") == entry_module.length - 3) - entry_module = entry_module.substring(0, entry_module.length - 3); - map_contents += `const EJSModule* entry_module = &${ - js_modules.get(entry_module).module_name - };\n`; - - map_contents += "};"; - fs.writeFileSync(map_path, map_contents); - - temp_files.push(map_path); - - return map_path; -} - -function do_final_link(main_file, modules) { - let js_modules = new Map(); - let native_modules = new Map(); - modules.forEach((m, k) => { - if (m.isNative()) { - native_modules.set(k, m); - } else { - js_modules.set(k, m); - } - }); - - let map_filename = generate_import_map(js_modules, native_modules); - - process.env.PATH = `${target_path_prepend(target_triple)}:${ - process.env.PATH - }`; - - let output_filename = options.output_filename || `${main_file}.exe`; - let clang_args = target_link_args(target_triple).concat( - [`-DEJS_BITS_PER_WORD=${target_triple.pointerSize()}`, "-o", output_filename].concat( - o_filenames - ) - ); - if (target_triple.isLittleEndian()) clang_args.unshift("-DIS_LITTLE_ENDIAN=1"); - - clang_args.push(`-I${relative_to_ejs_exe(options.srcdir ? "./runtime" : "../include")}`); - - clang_args.push(map_filename); - - clang_args = clang_args.concat( - relative_to_ejs_exe(target_libecho(target_triple)) - ); - clang_args = clang_args.concat( - relative_to_ejs_exe(target_extra_libs(target_triple)) - ); - - let seen_native_modules = new Set(); - native_modules.forEach((module) => { - // don't include native modules more than once - module.module_files.forEach((mf) => { - if (seen_native_modules.has(mf)) return; - - seen_native_modules.add(mf); - - clang_args.push( - path.resolve( - module.ejs_dir, - options.srcdir ? "." : `${target_triple.arch}-${target_triple.os}`, - mf - ) - ); - }); - - clang_args = clang_args.concat(module.link_flags.replace("\n", " ").split(" ")); - }); - - clang_args = clang_args.concat(target_libraries(target_triple)); - - if (!options.quiet) options.stdout_writer.write(`${bold()}LINK${reset()} ${output_filename}`); - - debug.log(1, `executing '${target_linker} ${clang_args.join(" ")}'`); - - if (typeof __ejs != "undefined") { - spawn(target_linker, clang_args); - // we ignore leave_tmp_files here - if (!options.quiet) console.warn(`${bold()}done.${reset()}`); - } else { - let clang = spawn(target_linker, clang_args); - clang.stderr.on("data", (data) => console.warn(`${data}`)); - clang.on("exit", (/* XXX code*/) => { - if (!options.leave_temp_files) { - cleanup(() => { - if (!options.quiet) console.warn(`${bold()}done.${reset()}`); - }); - } - }); - } -} - -function cleanup(done) { - let files_to_delete = temp_files.length; - temp_files.forEach((filename) => { - fs.unlink(filename, (/* XXX err*/) => { - files_to_delete = files_to_delete - 1; - if (files_to_delete === 0) done(); - }); - }); -} - -let main_file = file_args[0]; - -if (!options.srcdir) options.native_module_dirs.push(relative_to_ejs_exe("../lib")); -let files = gatherAllModules(file_args, options, target_triple); -debug.log(1, () => dumpModules()); -let allModules = getAllModules(); - -// now compile them -// -// reverse the list so the main program is the first thing we compile -files.reverse(); -let files_count = files.length; -let compileNextFile = () => { - if (files.length === 0) { - do_final_link(main_file, allModules); - return; - } - let f = files.pop(); - compileFile( - f.file_name, - f.file_ast, - allModules, - files_count, - files_count - files.length, - compileNextFile - ); -}; -compileNextFile(); diff --git a/ejs-es6.ts b/ejs-es6.ts new file mode 100644 index 00000000..8704989f --- /dev/null +++ b/ejs-es6.ts @@ -0,0 +1,990 @@ +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: + */ + +import * as os from "@node-compat/os"; +import * as path from "@node-compat/path"; +import * as fs from "@node-compat/fs"; +import * as child_process from "@node-compat/child_process"; +import type { CompilerOptions } from "./lib/options"; +import type { Triple as TripleT } from "./lib/triple"; +import type { ModuleInfo, JSModuleInfo, NativeModuleInfo } from "./lib/module-info"; +import type { Program } from "./lib/estree"; + +import * as debug from "./lib/debug"; +import { compile } from "./lib/compiler"; +import { dumpModules, getAllModules, gatherAllModules } from "./lib/passes/gather-imports"; + +import { bold, reset, genFreshFileName, Writer } from "./lib/echo-util"; +import { Triple } from "./lib/triple"; + +import { + LLVM_SUFFIX as DEFAULT_LLVM_SUFFIX, + LLVM_BINDIR as DEFAULT_LLVM_BINDIR, + LLVM_MAJOR as EXPECTED_LLVM_MAJOR, + RUNLOOP_IMPL as DEFAULT_RUNLOOP_IMPL, +} from "./lib/host-config"; +import { + formatEffectiveConfig, + formatPassHelp, + passes, + resolvePassConfig, + setPassConfig, +} from "./lib/pass-config"; + +const spawn = child_process.spawn; + +// the self-hosted runtime exposes a global marker object +declare const __ejs: object | undefined; + +function isNode(): boolean { + return typeof __ejs == "undefined"; +} + +let argv: string[]; +if (!isNode()) { + // argv is ['.../ejs', ...], get rid of the first arg + argv = process.argv.slice(1); +} else { + // argv is ['node', '.../ejs-es6.js', ...], get rid of the first two args + argv = process.argv.slice(2); +} + +let ejs_dirname: string | undefined; +function ejs_exe_dirname(): string { + if (ejs_dirname) return ejs_dirname; + const argv0 = process.argv[isNode() ? 1 : 0]!; + let cwd = process.cwd(); + + let full_path_to_exe: string | undefined; + if (argv0.indexOf("/") != -1) { + // either relative or absolute. don't both searching path. + let ejs_path = path.resolve(cwd, argv0); + try { + if (fs.statSync(ejs_path).isFile()) { + full_path_to_exe = ejs_path; + } + } catch (e) { + // an exception while stat'ing is the same as the file not existing. + } + } else { + // not qualified at all, search over PATH + for (const p of (process.env["PATH"] || "").split(":")) { + let ejs_path = path.resolve(cwd, p, argv0); + try { + if (fs.statSync(ejs_path).isFile()) { + full_path_to_exe = ejs_path; + break; + } + } catch (e) { + // we treat an exception while stat'ing the same as the file not existing. + } + } + } + if (!full_path_to_exe) { + throw new Error("could not locate ejs executable"); + } + + ejs_dirname = path.dirname(full_path_to_exe); + return ejs_dirname; +} + +function relative_to_ejs_exe(n: string): string; +function relative_to_ejs_exe(n: string[]): string[]; +function relative_to_ejs_exe(n: string | string[]): string | string[] { + const was_array = Array.isArray(n); + const list = was_array ? n : [n]; + + const rv = isNode() + ? list.map((el) => path.resolve(ejs_exe_dirname(), "../..", el)) + : list.map((el) => path.resolve(ejs_exe_dirname(), el)); + + if (was_array) return rv; + return rv[0]!; +} + +const temp_files: string[] = []; + +const host_triple = Triple.fromProcess(); +let target_triple = host_triple; // a reasonable default. we're compiling for _this_ triple. + +const options: CompilerOptions = { + // our defaults: + opt_level: 2, + debug: false, + debug_level: 0, + debug_passes: new Set(), + warn_on_undeclared: false, + frozen_global: false, + record_types: false, + types: false, + types_dump: false, + output_filename: null, + show_help: false, + leave_temp_files: false, + native_module_dirs: [], + extra_clang_args: "", + ios_sdk: "9.2", + ios_min: "8.0", + osx_min: "11.0", + import_variables: [], + srcdir: false, + stdout_writer: new Writer(process.stdout), +}; + +function add_native_module_dir(dir: string): void { + options.native_module_dirs.push(dir); +} + +function set_target(str: string): void { + let triple: TripleT; + switch (str) { + case "linux_x86_64": + triple = new Triple({ arch: "x86_64", vendor: "unknown", os: "linux" }); + break; + case "macos": + triple = new Triple({ arch: "arm64", vendor: "apple", os: "macos" }); + break; + case "iossim": + triple = new Triple({ arch: "arm64", vendor: "apple", os: "ios", env: "simulator" }); + break; + case "iosdev": + triple = new Triple({ arch: "arm64", vendor: "apple", os: "ios" }); + break; + default: + triple = Triple.fromString(str); + break; + } + target_triple = triple; +} + +function set_extra_clang_args(arginfo: string): void { + options.extra_clang_args = arginfo; +} + +function increase_debug_level(): void { + options.debug_level += 1; +} + +function add_debug_after_pass(passname: string): void { + options.debug_passes.add(passname); +} + +function add_import_variable(arg: string): void { + let equal_idx = arg.indexOf("="); + if (equal_idx == -1) throw new Error("-I flag requires ="); + + options.import_variables.push({ + variable: arg.substring(0, equal_idx), + value: arg.substring(equal_idx + 1), + }); +} + +interface ArgSpec { + // sets options[flag] = true + flag?: keyof CompilerOptions & string; + // consumes one argument into options[option] + option?: keyof CompilerOptions & string; + handler?: (...args: string[]) => void; + handlerArgc?: number; + help: string; +} + +const args: Record = { + "-O0": { + handler: () => (options.opt_level = 0), + help: "straight lowering: no EIR optimizer, LLVM O0.", + }, + "-O1": { + handler: () => (options.opt_level = 1), + help: "the cheap always-sound EIR tier (cleanup, CSE, sinking), LLVM O1.", + }, + "-O2": { + handler: () => (options.opt_level = 2), + help: "the full EIR pipeline (adds the module-level tier), LLVM O2 (default).", + }, + "-O3": { + handler: () => (options.opt_level = 3), + help: "same EIR suite as -O2, LLVM O3.", + }, + "-g": { + flag: "debug", + help: "enable debugging of generated code", + }, + "-q": { + flag: "quiet", + help: "don't output anything during compilation except errors.", + }, + "-I": { + handler: add_import_variable, + handlerArgc: 1, + help: "add a name=value mapping used to resolve module references.", + }, + "-d": { + handler: increase_debug_level, + handlerArgc: 0, + help: "debug output. more instances of this flag increase the amount of spew.", + }, + "--dump-after": { + handler: add_debug_after_pass, + handlerArgc: 1, + help: "dump the AST after the named pass; `--dump-after eir` dumps the lowered EIR module(s), `--dump-after eir-opt` the optimized EIR", + }, + "--debug-after": { + handler: add_debug_after_pass, + handlerArgc: 1, + help: "deprecated alias for --dump-after", + }, + "-o": { + option: "output_filename", + help: "name of the output file.", + }, + "--leave-temp": { + flag: "leave_temp_files", + help: "leave temporary files in $TMPDIR from compilation", + }, + "--moduledir": { + handler: add_native_module_dir, + handlerArgc: 1, + help: "--module path-to-search-for-modules", + }, + "--help": { + flag: "show_help", + help: "output this help info.", + }, + "--extra-clang-args": { + handler: set_extra_clang_args, + handlerArgc: 1, + help: "extra arguments to pass to the clang command (used to compile the .s to .o)", + }, + "--record-types": { + flag: "record_types", + help: "generates an executable which records types in a format later used for optimizations.", + }, + "--types": { + flag: "types", + help: "run the MAAM type-analysis probe over each module and log its stats (consumes nothing yet).", + }, + "--types-dump": { + flag: "types_dump", + help: "with the MAAM analysis, print each binding's inferred type (implies --types).", + }, + "--frozen-global": { + flag: "frozen_global", + help: "compiler acts as if the global object is frozen after initialization, allowing for faster access.", + }, + "--warn-on-undeclared": { + flag: "warn_on_undeclared", + help: "accesses to undeclared identifiers result in warnings (and global accesses). By default they're an error.", + }, + "--target": { + handler: set_target, + handlerArgc: 1, + help: "--target linux_x86_64|macos|iossim|iosdev", + }, + "--ios-sdk": { + option: "ios_sdk", + help: "the version of the ios sdk to use. useful if more than one is installed. Default is 7.0.", + }, + "--ios-min": { + option: "ios_min", + help: "the minimum version of iOS to support. Default is 8.0.", + }, + "--osx-min": { + option: "osx_min", + help: "the minimum version of OSX to support. Default is 11.0.", + }, + "--srcdir": { + flag: "srcdir", + help: "internal flag. if set, will look for libecho/libpcre/etc from source directory locations.", + }, + "--print-passes": { + handler: () => (print_passes = true), + handlerArgc: 0, + help: "print the effective pass configuration (after the -O suite and any -f flags) and exit.", + }, +}; + +// -f/-fno- tokens, in command-line order (applied after the +// -O suite by resolvePassConfig; EJS_FLAGS tokens land at the end, so +// the env escape wins for bisecting) +const pass_flag_tokens: string[] = []; +let print_passes = false; + +function output_usage() { + console.warn("Usage:"); + console.warn(" ejs [options] file1.js file2.js file.js ..."); +} + +function output_options() { + console.warn("Options:"); + for (const a of Object.keys(args)) { + console.warn(` ${a}: ${args[a]!.help}`); + } +} + +let file_args: string[] | undefined; + +if (argv.length > 0) { + for (let ai = 0, ae = argv.length; ai < ae; ai++) { + // pass flags are prefix-matched (every other option is an exact + // table key; none start with -f) + if (argv[ai]!.indexOf("-f") === 0) { + pass_flag_tokens.push(argv[ai]!); + continue; + } + const o = args[argv[ai]!]; + if (o) { + const opts = options as unknown as Record; + if (o.flag) { + opts[o.flag] = true; + } else if (o.option) { + opts[o.option] = argv[++ai]!; + } else if (o.handler) { + const handler_args: string[] = []; + for (let i = 0, e = o.handlerArgc ?? 0; i < e; i++) handler_args.push(argv[++ai]!); + o.handler.apply(null, handler_args); + } + } else { + // end of options signals the rest of the array is files + file_args = argv.slice(ai); + break; + } + } +} + +// EJS_FLAGS: extra pass-configuration argv from the environment, for +// bisecting inside harnesses that don't thread driver flags. Applied +// after the real command line (so it wins), and restricted to -O/-f +// tokens — it configures the optimizer, nothing else. +for (const token of (process.env["EJS_FLAGS"] || "").split(/\s+/)) { + if (token.length === 0) continue; + const o = args[token]; + if (token.indexOf("-f") === 0) { + pass_flag_tokens.push(token); + } else if (o && token.indexOf("-O") === 0) { + o.handler!(); + } else { + console.warn(`EJS_FLAGS supports only -O and -f flags, got '${token}'`); + process.exit(-1); + } +} + +const resolved_passes = resolvePassConfig(options.opt_level, pass_flag_tokens); +if (resolved_passes.errors.length > 0) { + for (const err of resolved_passes.errors) console.warn(err); + process.exit(-1); +} +setPassConfig(resolved_passes.config); + +if (options.show_help) { + output_usage(); + console.warn(""); + output_options(); + console.warn(""); + console.warn(formatPassHelp()); + process.exit(0); +} + +if (print_passes) { + console.log(formatEffectiveConfig(resolved_passes, options.opt_level)); + process.exit(0); +} + +if (!file_args || file_args.length === 0) { + output_usage(); + process.exit(0); +} + +if (!options.quiet) { + console.log(`host: ${host_triple.toShortString()}, target: ${target_triple.toShortString()}`); +} + +debug.setLevel(options.debug_level); + +const o_filenames: string[] = []; + +const compiled_modules: { filename: string; module_toplevel: string }[] = []; + +let sim_base = "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform"; +let dev_base = "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform"; + +let sim_bin = `${sim_base}/Developer/usr/bin`; +let dev_bin = `${dev_base}/Developer/usr/bin`; + +function target_llc_args(triple: TripleT): string[] { + let args = [`-march=${triple.llcArch()}`]; + switch (triple.os) { + case "macos": + args = args.concat([ + `-mtriple=arm64-apple-macosx${options.osx_min}.0`, + "-mattr=+fp-armv8", + "--relocation-model=pic", + ]); + break; + case "ios": + args = args.concat([`-mtriple=arm64-apple-ios${options.ios_min}.0`]); + break; + case "linux": + args = args.concat(["--relocation-model=pic"]); + break; + } + + return args; +} + +const target_linker = process.env["CXX"] || "clang++"; + +function target_link_args(triple: TripleT): string[] { + let args = ["-arch", triple.clangArch()]; + + if (triple.os === "linux") { + // -arch is a Darwin-only clang flag. -no-pie keeps the data + // segment (static atom strings get NaN-boxed by address) below + // the 47-bit ejsval payload limit; PIE ASLR on aarch64 maps it + // above 2^47. + return ["-no-pie"]; + } + + if (triple.os === "macos") { + return args; + } + + if (triple.os === "ios") { + if (triple.env === "simulator") { + return args.concat([ + "-isysroot", + `${sim_base}/Developer/SDKs/iPhoneSimulator${options.ios_sdk}.sdk`, + `-miphoneos-version-min=${options.ios_min}`, + ]); + } + + return args.concat([ + "-isysroot", + `${dev_base}/Developer/SDKs/iPhoneOS${options.ios_sdk}.sdk`, + `-miphoneos-version-min=${options.ios_min}`, + ]); + } + + return []; +} + +function target_libraries(triple: TripleT): string[] { + if (triple.os === "linux") { + if (DEFAULT_RUNLOOP_IMPL == "noop") return ["-lunwind", "-lpthread"]; + return ["-lunwind", "-lpthread", "-luv"]; + } + + if (triple.os === "macos") { + // for macos we only need Foundation and AppKit + return ["-framework", "Foundation", "-framework", "AppKit"]; + } + + if (triple.os === "ios") { + return [ + "-framework", + "Foundation", + "-framework", + "UIKit", + "-framework", + "GLKit", + "-framework", + "OpenGLES", + "-framework", + "CoreGraphics", + ]; + } + return []; +} + +function target_libecho(triple: TripleT): string { + if (options.srcdir) { + return path.join("runtime", "out", `${triple}`, "libecho.a"); + } else { + return path.join(relative_to_ejs_exe(`../lib/${triple}`), "libecho.a"); + } +} + +function target_extra_libs(triple: TripleT): string[] { + if (options.srcdir) { + if (triple.os === "linux") + return [ + "external-deps/double-conversion-linux/double-conversion/libdouble-conversion.a", + "external-deps/pcre-linux/.libs/libpcre16.a", + ]; + + if (triple.os === "macos") { + return [ + "external-deps/double-conversion-macos/double-conversion/libdouble-conversion.a", + "external-deps/pcre-macos/.libs/libpcre16.a", + ]; + } + + if (triple.os === "ios") { + if (triple.env === "simulator") { + return [ + "external-deps/double-conversion-iossim/double-conversion/libdouble-conversion.a", + "external-deps/pcre-iossim/.libs/libpcre16.a", + ]; + } + + return [ + "external-deps/double-conversion-iosdev/double-conversion/libdouble-conversion.a", + "external-deps/pcre-iosdev/.libs/libpcre16.a", + ]; + } + + throw new Error("no pcre for this platform"); + } else { + return ["libdouble-conversion.a", "libpcre16.a"].map((lib) => + path.join(relative_to_ejs_exe(`../lib/${triple}`), lib) + ); + } +} + +function target_path_prepend(triple: TripleT): string { + if (triple.os === "ios") { + if (triple.env === "simulator") { + return sim_bin; + } + return dev_bin; + } + return ""; +} + +const llvm_suffix = process.env["LLVM_SUFFIX"] || DEFAULT_LLVM_SUFFIX; + +// The LLVM toolchain policy (release-P1): spawn opt/llc only from a +// bindir whose major version matches the one this compiler was BUILT +// against (baked into host-config) — a different-major `opt` reading +// our bitcode doesn't fail loudly; llvm@16 turned llvm-22 module-init +// stores into `unreachable` traps with exit code 0. Resolution order: +// 1. LLVM_BINDIR in the environment ("" = plain PATH lookup) — the +// explicit override, still version-checked; +// 2. the baked build-machine bindir; +// 3. conventional install locations for the host os, then PATH. +// Every candidate is verified by running `opt --version`; if nothing +// compatible is found the driver fails loudly instead of miscompiling. +// EJS_LLVM_NO_VERSION_CHECK=1 skips the probe (debugging only). + +// capture `opt --version` through a shell redirect to a temp file: the +// self-hosted spawn is synchronous and returns only the exit status, so +// this is the one output-capture mechanism both hosts share +function probeLlvmMajor(opt_path: string): string | null { + const probe_file = `${os.tmpdir()}/${genFreshFileName("ejs-llvm-probe")}.txt`; + temp_files.push(probe_file); + const sh_cmd = `"${opt_path}" --version > "${probe_file}" 2>&1`; + let status: number; + if (isNode()) { + status = child_process.spawnSync("/bin/sh", ["-c", sh_cmd]).status ?? -1; + } else { + status = spawn("/bin/sh", ["-c", sh_cmd]) as unknown as number; + } + if (status !== 0) return null; + let version_text: string; + try { + version_text = fs.readFileSync(probe_file, "utf-8").toString(); + } catch (e) { + return null; + } + const m = version_text.match(/LLVM version (\d+)\./); + return m ? m[1]! : null; +} + +function llvm_bindir_candidates(): string[] { + const candidates = [DEFAULT_LLVM_BINDIR]; + if (host_triple.os === "macos") { + for (const prefix of ["/opt/homebrew/opt", "/usr/local/opt"]) { + candidates.push(`${prefix}/llvm@${EXPECTED_LLVM_MAJOR}/bin`); + candidates.push(`${prefix}/llvm/bin`); + } + } else if (host_triple.os === "linux") { + candidates.push(`/usr/lib/llvm-${EXPECTED_LLVM_MAJOR}/bin`); + } + candidates.push(""); // last resort: whatever PATH resolves + return candidates.filter((c, i) => candidates.indexOf(c) === i); +} + +let resolved_llvm_bindir: string | undefined; +function llvm_bindir(): string { + if (resolved_llvm_bindir !== undefined) return resolved_llvm_bindir; + const opt_name = "opt" + llvm_suffix; + const opt_in = (bindir: string): string => (bindir ? path.join(bindir, opt_name) : opt_name); + const env_bindir = process.env["LLVM_BINDIR"]; + const skip_check = process.env["EJS_LLVM_NO_VERSION_CHECK"] === "1"; + + if (env_bindir !== undefined) { + if (!skip_check) { + const found = probeLlvmMajor(opt_in(env_bindir)); + if (found !== EXPECTED_LLVM_MAJOR) { + console.warn( + `error: LLVM_BINDIR=${env_bindir || "(PATH lookup)"} provides ${ + found === null ? `no working ${opt_name}` : `LLVM ${found}` + }; this compiler requires LLVM ${EXPECTED_LLVM_MAJOR}.` + ); + console.warn( + `a mismatched opt/llc can miscompile silently; set EJS_LLVM_NO_VERSION_CHECK=1 to force (debugging only).` + ); + process.exit(-1); + } + } + resolved_llvm_bindir = env_bindir; + return resolved_llvm_bindir; + } + + if (skip_check) { + resolved_llvm_bindir = DEFAULT_LLVM_BINDIR; + return resolved_llvm_bindir; + } + + const tried: string[] = []; + for (const candidate of llvm_bindir_candidates()) { + if (candidate !== "") { + let present = false; + try { + present = fs.statSync(opt_in(candidate)).isFile(); + } catch (e) { + // missing is the common case; fall through + } + if (!present) { + tried.push(`${candidate} (no ${opt_name})`); + continue; + } + } + const found = probeLlvmMajor(opt_in(candidate)); + if (found === EXPECTED_LLVM_MAJOR) { + resolved_llvm_bindir = candidate; + return resolved_llvm_bindir; + } + tried.push(`${candidate || "$PATH"} (${found === null ? `no working ${opt_name}` : `LLVM ${found}`})`); + } + + console.warn( + `error: could not find the LLVM ${EXPECTED_LLVM_MAJOR} tools (${opt_name}, llc${llvm_suffix}) this compiler requires.` + ); + for (const t of tried) console.warn(` tried: ${t}`); + console.warn( + `install LLVM ${EXPECTED_LLVM_MAJOR} (macos: \`brew install llvm@${EXPECTED_LLVM_MAJOR}\`; linux: https://apt.llvm.org) or set LLVM_BINDIR to its bin directory.` + ); + process.exit(-1); + throw new Error("unreachable"); +} + +const llvm_tool = (tool: string): string => { + const bindir = llvm_bindir(); + return bindir ? path.join(bindir, tool + llvm_suffix) : tool + llvm_suffix; +}; + +// the self-hosted runtime's spawn is synchronous and returns the child's +// exit status (a number); node's returns a ChildProcess. This helper is +// for the self-hosted branches: run the tool, fail the build loudly on a +// non-zero exit instead of continuing to link stale objects. +function spawnSyncChecked(command: string, cmd_args: string[]): void { + const rv = spawn(command, cmd_args) as unknown as number; + if (rv !== 0) { + console.warn(`${command} failed (exit status ${rv})`); + process.exit(-1); + } +} + +function compileFile( + filename: string, + parse_tree: Program, + modules: Map, + files_count: number, + cur_file: number, + compileCallback: () => void +): void { + let base_filename = genFreshFileName(path.basename(filename)); + + if (!options.quiet) { + let suffix = options.debug_level > 0 ? ` -> ${base_filename}` : ""; + + // loop over import variables, replacing their values with + // their names for output + let output_name = filename; + for (let ivar of options.import_variables) { + output_name = output_name.replace(ivar.value, `$${ivar.variable}`); + } + options.stdout_writer.write( + `[${cur_file}/${files_count}] ${bold()}COMPILE${reset()} ${output_name}${suffix}` + ); + } + + let compiled_module: import("@llvm").Module; + try { + compiled_module = compile( + parse_tree, + base_filename, + filename, + modules, + options, + target_triple + ); + } catch (e) { + console.warn(`${e}`); + if (options.debug_level == 0) process.exit(-1); + throw e; + } + + function tmpfile(suffix: string): string { + return `${os.tmpdir()}/${base_filename}-${target_triple.arch}-${target_triple.os}${suffix}`; + } + let bc_filename = tmpfile(".bc"); + let bc_opt_filename = tmpfile(".bc.opt"); + let o_filename = tmpfile(".o"); + + temp_files.push(bc_filename, bc_opt_filename, o_filename); + + // the LLVM pipeline follows the -O level unless -fllvm-opt decouples it + const llvm_opt = passes().llvmOpt ?? options.opt_level; + let opt_level = llvm_opt > 0 ? `default,` : ""; + + // bitcode end to end: the module serializes straight to .bc (no + // llvm-as spawn, no textual round trip), opt reads and emits bitcode + // (no -S), and llc consumes the optimized bitcode. Both binding sets + // (node-llvm and the self-hosted ejs-llvm) expose writeBitcodeToFile, + // so stage0 and stage1+ run the identical pipeline. + let opt_args = [`-passes=${opt_level}strip-dead-prototypes`, `-o=${bc_opt_filename}`, bc_filename]; + let llc_args = target_llc_args(target_triple).concat([ + "-filetype=obj", + `-o=${o_filename}`, + bc_opt_filename, + ]); + + debug.log(1, `writing ${bc_filename}`); + compiled_module.writeBitcodeToFile(bc_filename); + debug.log(1, `done writing ${bc_filename}`); + + // textual IR is a debug artifact now: written only under --leave-temp + // (buck-test-lowtier.sh greps it for the low-tier float ops — the same + // pre-opt module dump the old pipeline fed to llvm-as) + if (options.leave_temp_files) { + let ll_filename = tmpfile(".ll"); + temp_files.push(ll_filename); + debug.log(1, `writing ${ll_filename}`); + compiled_module.writeToFile(ll_filename); + } + + compiled_modules.push({ + filename: filename, + module_toplevel: (compiled_module as unknown as { toplevel_name: string }).toplevel_name, + }); + + const opt_cmd = llvm_tool("opt"); + const llc_cmd = llvm_tool("llc"); + if (!isNode()) { + // in ejs spawn is synchronous. + spawnSyncChecked(opt_cmd, opt_args); + spawnSyncChecked(llc_cmd, llc_args); + o_filenames.push(o_filename); + compileCallback(); + } else { + debug.log(1, `executing '${opt_cmd} ${opt_args.join(" ")}'`); + let opt = spawn(opt_cmd, opt_args); + opt.stderr.on("data", (data) => console.warn(`${data}`)); + opt.on("error", (err) => { + console.warn(`error executing ${opt_cmd}: ${err}`); + process.exit(-1); + }); + opt.on("exit", (code) => { + if (code !== 0) { + console.warn(`${opt_cmd} failed (exit status ${code})`); + process.exit(-1); + } + debug.log(1, `executing '${llc_cmd} ${llc_args.join(" ")}'`); + let llc = spawn(llc_cmd, llc_args); + llc.stderr.on("data", (data) => console.warn(`${data}`)); + llc.on("error", (err) => { + console.warn(`error executing ${llc_cmd}: ${err}`); + process.exit(-1); + }); + llc.on("exit", (code) => { + if (code !== 0) { + console.warn(`${llc_cmd} failed (exit status ${code})`); + process.exit(-1); + } + o_filenames.push(o_filename); + compileCallback(); + }); + }); + } +} + +function generate_import_map( + js_modules: Map, + native_modules: Map +): string { + let map_path = `${os.tmpdir()}/${genFreshFileName(path.basename(main_file))}-import-map.cpp`; + + let map_contents = ""; + map_contents += `#include "ejs-module.h"\n`; + map_contents += 'extern "C" {\n'; + + js_modules.forEach((module) => { + map_contents += `extern EJSModule ${module.module_name};\n`; + map_contents += `extern ejsval ${module.toplevel_function_name} (ejsval env, ejsval _this, uint32_t argc, ejsval *args);\n`; + }); + + map_contents += "EJSModule* _ejs_modules[] = {\n"; + js_modules.forEach((module) => { + map_contents += ` &${module.module_name},\n`; + }); + map_contents += "};\n"; + + map_contents += "ejsval (*_ejs_module_toplevels[])(ejsval, ejsval, uint32_t, ejsval*) = {\n"; + js_modules.forEach((module) => { + map_contents += ` ${module.toplevel_function_name},\n`; + }); + map_contents += "};\n"; + map_contents += "int _ejs_num_modules = sizeof(_ejs_modules) / sizeof(_ejs_modules[0]);\n\n"; + + native_modules.forEach((module) => { + map_contents += `extern ejsval ${module.init_function} (ejsval exports);\n`; + }); + + map_contents += "EJSExternalModule _ejs_external_modules[] = {\n"; + native_modules.forEach((module) => { + map_contents += ` { "@${module.module_name}", ${module.init_function}, 0 },\n`; + }); + map_contents += "};\n"; + map_contents += + "int _ejs_num_external_modules = sizeof(_ejs_external_modules) / sizeof(_ejs_external_modules[0]);\n"; + + let entry_module = file_args![0]!; + if (entry_module.lastIndexOf(".js") == entry_module.length - 3) + entry_module = entry_module.substring(0, entry_module.length - 3); + map_contents += `const EJSModule* entry_module = &${ + js_modules.get(entry_module)!.module_name + };\n`; + + map_contents += "};"; + fs.writeFileSync(map_path, map_contents); + + temp_files.push(map_path); + + return map_path; +} + +function do_final_link(main_file: string, modules: Map): void { + const js_modules = new Map(); + const native_modules = new Map(); + modules.forEach((m, k) => { + if (m.isNative()) { + native_modules.set(k, m as NativeModuleInfo); + } else { + js_modules.set(k, m as JSModuleInfo); + } + }); + + let map_filename = generate_import_map(js_modules, native_modules); + + process.env["PATH"] = `${target_path_prepend(target_triple)}:${process.env["PATH"]}`; + + let output_filename = options.output_filename || `${main_file}.exe`; + let clang_args = target_link_args(target_triple).concat( + [`-DEJS_BITS_PER_WORD=${target_triple.pointerSize()}`, "-o", output_filename].concat( + o_filenames + ) + ); + if (target_triple.isLittleEndian()) clang_args.unshift("-DIS_LITTLE_ENDIAN=1"); + + clang_args.push(`-I${relative_to_ejs_exe(options.srcdir ? "./runtime" : "../include")}`); + + clang_args.push(map_filename); + + clang_args = clang_args.concat(relative_to_ejs_exe(target_libecho(target_triple))); + clang_args = clang_args.concat(relative_to_ejs_exe(target_extra_libs(target_triple))); + + const seen_native_modules = new Set(); + native_modules.forEach((module) => { + // don't include native modules more than once + module.module_files.forEach((mf) => { + if (seen_native_modules.has(mf)) return; + + seen_native_modules.add(mf); + + clang_args.push( + path.resolve( + module.ejs_dir, + options.srcdir ? "." : `${target_triple.arch}-${target_triple.os}`, + mf + ) + ); + }); + + clang_args = clang_args.concat(module.link_flags.replace("\n", " ").split(" ")); + }); + + clang_args = clang_args.concat(target_libraries(target_triple)); + + if (!options.quiet) options.stdout_writer.write(`${bold()}LINK${reset()} ${output_filename}`); + + debug.log(1, `executing '${target_linker} ${clang_args.join(" ")}'`); + + if (typeof __ejs != "undefined") { + spawnSyncChecked(target_linker, clang_args); + // we ignore leave_tmp_files here + if (!options.quiet) console.warn(`${bold()}done.${reset()}`); + } else { + let clang = spawn(target_linker, clang_args); + clang.stderr.on("data", (data) => console.warn(`${data}`)); + clang.on("error", (err) => { + console.warn(`error executing ${target_linker}: ${err}`); + process.exit(-1); + }); + clang.on("exit", (code) => { + if (code !== 0) { + console.warn(`${target_linker} failed (exit status ${code})`); + process.exit(-1); + } + if (!options.leave_temp_files) { + cleanup(() => { + if (!options.quiet) console.warn(`${bold()}done.${reset()}`); + }); + } + }); + } +} + +function cleanup(done: () => void): void { + let files_to_delete = temp_files.length; + temp_files.forEach((filename) => { + fs.unlink(filename, (/* XXX err*/) => { + files_to_delete = files_to_delete - 1; + if (files_to_delete === 0) done(); + }); + }); +} + +const main_file = file_args[0]!; + +if (!options.srcdir) options.native_module_dirs.push(relative_to_ejs_exe("../lib")); +let files = gatherAllModules(file_args, options, target_triple); +debug.log(1, () => { + dumpModules(); + return ""; +}); +let allModules = getAllModules(); + +// now compile them +// +// reverse the list so the main program is the first thing we compile +files.reverse(); +let files_count = files.length; +const compileNextFile = (): void => { + if (files.length === 0) { + do_final_link(main_file, allModules); + return; + } + const f = files.pop()!; + compileFile( + f.file_name, + f.file_ast, + allModules, + files_count, + files_count - files.length, + compileNextFile + ); +}; +compileNextFile(); diff --git a/ejs-llvm/BUCK b/ejs-llvm/BUCK new file mode 100644 index 00000000..b998585d --- /dev/null +++ b/ejs-llvm/BUCK @@ -0,0 +1,73 @@ +load("//:defs.bzl", "EJS_COMPILER_FLAGS", "llvm_bin", "llvm_prefix") + +genrule( + name = "atoms", + srcs = [ + "ejs-llvm-atoms.h", + "//runtime:gen-atoms-js", + ], + out = "ejs-llvm-atoms-gen.c", + cmd = "node $(location //runtime:gen-atoms-js) $SRCDIR/ejs-llvm-atoms.h > $OUT", +) + +# The .ejs module descriptor, with the link flags the EJS compiler must pass +# when linking a program that imports @llvm. +genrule( + name = "ejs-llvm.ejs", + srcs = ["ejs-llvm.ejs.in"], + out = "ejs-llvm.ejs", + cmd = 'set -e; LINK_FLAGS="`' + llvm_bin("llvm-config") + " --ldflags --libs | tr '\\n' ' '`" + + select({ + "DEFAULT": "", + "config//os:macos": " -lcurses", + }) + + '"; sed -e "s%@EJS_VERSION@%0.1.0%" -e "s%@LLVM_LINK_FLAGS@%$LINK_FLAGS%" $SRCDIR/ejs-llvm.ejs.in > $OUT', + visibility = ["PUBLIC"], +) + +sources = [ + "allocainst.cpp", + "arraytype.cpp", + "basicblock.cpp", + "callinvoke.cpp", + "constant.cpp", + "constantarray.cpp", + "constantfp.cpp", + "dibuilder.cpp", + "ejs-llvm.cpp", + "function.cpp", + "functiontype.cpp", + "globalvariable.cpp", + "irbuilder.cpp", + "landingpad.cpp", + "phinode.cpp", + "loadinst.cpp", + "module.cpp", + "structtype.cpp", + "switch.cpp", + "type.cpp", + "value.cpp", +] + +cxx_library( + name = "ejs-llvm", + srcs = sources, + header_namespace = "", + headers = dict( + [(h, h) for h in glob(["*.h"])] + + [("ejs-llvm-atoms-gen.c", ":atoms")], + ), + compiler_flags = EJS_COMPILER_FLAGS + [ + "-std=c++17", + "-I" + llvm_prefix() + "/include", + "-fno-rtti", + "-D__STDC_CONSTANT_MACROS", + "-D__STDC_FORMAT_MACROS", + "-D__STDC_LIMIT_MACROS", + "-Wno-c99-extensions", + "-Wno-gnu-statement-expression", + ], + preferred_linkage = "static", + deps = ["//runtime:echo"], + visibility = ["PUBLIC"], +) diff --git a/ejs-llvm/Makefile b/ejs-llvm/Makefile deleted file mode 100644 index 2627685f..00000000 --- a/ejs-llvm/Makefile +++ /dev/null @@ -1,81 +0,0 @@ -TOP=.. - -include $(TOP)/build/config.mk - -SOURCES= \ - allocainst.cpp \ - arraytype.cpp \ - basicblock.cpp \ - callinvoke.cpp \ - constant.cpp \ - constantarray.cpp \ - constantfp.cpp \ - dibuilder.cpp \ - ejs-llvm.cpp \ - function.cpp \ - functiontype.cpp \ - globalvariable.cpp \ - irbuilder.cpp \ - landingpad.cpp \ - loadinst.cpp \ - module.cpp \ - structtype.cpp \ - switch.cpp \ - type.cpp \ - value.cpp - - -CXX=clang++ - -LLVM_CONFIG=llvm-config$(LLVM_SUFFIX) - -LLVM_CXXFLAGS := $(shell $(LLVM_CONFIG) --cxxflags) -LLVM_INCLUDEDIR := $(shell $(LLVM_CONFIG) --includedir) - -LLVM_CXXFLAGS := $(subst $(LLVM_CPPFLAGS),,$(LLVM_CXXFLAGS)) -LLVM_DEFINES := $(subst -I$(LLVM_INCLUDEDIR),,$(LLVM_CPPFLAGS)) - -LLVM_LINK_FLAGS := $(shell $(LLVM_CONFIG) --ldflags --libs) - -ifeq ($(HOST_OS),darwin) -LLVM_LINK_FLAGS := $(LLVM_LINK_FLAGS) -lcurses -endif - -CXXFLAGS=-I../runtime -I$(LLVM_INCLUDEDIR) $(LLVM_CXXFLAGS) $(OSX_CFLAGS) - -CFLAGS += -Wno-c99-extensions -Wno-gnu-statement-expression - -OBJECTS=$(SOURCES:%.cpp=%.o) - -MODULE=libejsllvm-module.a - -all-local:: ejs-llvm.ejs $(MODULE) - -$(MODULE): $(OBJECTS) - ar cru $@ $(OBJECTS) - -$(OBJECTS): %.o: %.cpp - @mkdir -p .deps - @$(CXX) -MM $(CXXFLAGS) $< > .deps/$@-deps - @echo [$(CXX)] $< && $(CXX) $(CXXFLAGS) -c $< -o $@ - -ejs-llvm.ejs: ejs-llvm.ejs.in - @echo [gen] $@ && sed -e "s%@EJS_VERSION@%$(PRODUCT_VERSION)%" -e "s%@LLVM_LINK_FLAGS@%$(LLVM_LINK_FLAGS)%" $< > $@ - -install-local:: - @$(MKDIR) $(libdir) - @$(MKDIR) $(archlibdir) - $(INSTALL) -c ejs-llvm.ejs $(libdir) - $(INSTALL) -c $(MODULE) $(archlibdir) - -clean-local:: - rm -f $(OBJECTS) $(MODULE) ejs-llvm-atoms-gen.c - -ejs-llvm-atoms-gen.c: ejs-llvm-atoms.h $(TOP)/runtime/gen-atoms.js - @echo [GEN] $@ && $(TOP)/runtime/gen-atoms.js $< > .tmp-$@ && mv .tmp-$@ $@ - -ejs-llvm.o: ejs-llvm-atoms-gen.c - --include $(patsubst %.o,.deps/%.o-deps,$(OBJECTS)) - -include $(TOP)/build/build.mk diff --git a/ejs-llvm/arraytype.cpp b/ejs-llvm/arraytype.cpp index 45597933..15606fc5 100644 --- a/ejs-llvm/arraytype.cpp +++ b/ejs-llvm/arraytype.cpp @@ -78,6 +78,8 @@ namespace ejsllvm { _ejs_gc_add_root (&_ejs_ArrayType_prototype); _ejs_ArrayType_prototype = _ejs_object_new(_ejs_Object_prototype, &_ejs_ArrayType_specops); + _ejs_gc_add_root (&_ejs_ArrayType); + _ejs_ArrayType = _ejs_function_new_utf8_with_proto (_ejs_null, "LLVMArrayType", (EJSClosureFunc)ArrayType_impl, _ejs_ArrayType_prototype); _ejs_object_setprop_utf8 (exports, "ArrayType", _ejs_ArrayType); diff --git a/ejs-llvm/basicblock.cpp b/ejs-llvm/basicblock.cpp index f5e9e73c..685a59c5 100644 --- a/ejs-llvm/basicblock.cpp +++ b/ejs-llvm/basicblock.cpp @@ -102,6 +102,8 @@ namespace ejsllvm { _ejs_gc_add_root (&_ejs_BasicBlock_prototype); _ejs_BasicBlock_prototype = _ejs_object_new(_ejs_Object_prototype, &_ejs_BasicBlock_specops); + _ejs_gc_add_root (&_ejs_BasicBlock); + _ejs_BasicBlock = _ejs_function_new_utf8_with_proto (_ejs_null, "LLVMBasicBlock", (EJSClosureFunc)BasicBlock_impl, _ejs_BasicBlock_prototype); _ejs_object_setprop_utf8 (exports, "BasicBlock", _ejs_BasicBlock); diff --git a/ejs-llvm/callinvoke.cpp b/ejs-llvm/callinvoke.cpp index 72bebe5a..f3ee5664 100644 --- a/ejs-llvm/callinvoke.cpp +++ b/ejs-llvm/callinvoke.cpp @@ -105,6 +105,7 @@ namespace ejsllvm { _ejs_Call_prototype = _ejs_object_new(_ejs_Object_prototype, &_ejs_Call_specops); ejsval tmpobj = _ejs_function_new_utf8 (_ejs_null, "LLVMCall", (EJSClosureFunc)Call_impl); + _ejs_gc_add_root (&_ejs_Call); _ejs_Call = tmpobj; @@ -211,6 +212,8 @@ namespace ejsllvm { _ejs_gc_add_root (&_ejs_Invoke_prototype); _ejs_Invoke_prototype = _ejs_object_new(_ejs_Object_prototype, &_ejs_Invoke_specops); + _ejs_gc_add_root (&_ejs_Invoke); + _ejs_Invoke = _ejs_function_new_utf8_with_proto (_ejs_null, "LLVMInvoke", (EJSClosureFunc)Invoke_impl, _ejs_Invoke_prototype); _ejs_object_setprop_utf8 (exports, "Invoke", _ejs_Invoke); diff --git a/ejs-llvm/constant.cpp b/ejs-llvm/constant.cpp index 8c145850..5a173d74 100644 --- a/ejs-llvm/constant.cpp +++ b/ejs-llvm/constant.cpp @@ -73,11 +73,17 @@ namespace ejsllvm { REQ_INT_ARG (1, v); if (argc == 2) { - return Value_new (llvm::Constant::getIntegerValue(ty, llvm::APInt(ty->getPrimitiveSizeInBits(), v))); + // llvm 20+ asserts on implicit truncation; keep the old + // truncating behavior for negative/oversized js numbers + return Value_new (llvm::Constant::getIntegerValue(ty, llvm::APInt(ty->getPrimitiveSizeInBits(), v, /*isSigned*/ true, /*implicitTrunc*/ true))); } else if (argc == 3 && EJSVAL_IS_NUMBER(args[2]) && ty->getPrimitiveSizeInBits() == 64) { uint64_t vhi = v; - uint32_t vlo = (uint32_t)EJSVAL_TO_NUMBER(args[2]); + // convert with ToUint32 (wrapping) semantics: a bare + // double->uint32_t cast of a negative value saturates to 0 on + // arm64, silently corrupting constants like 0xffffffff that + // reach us as -1 + uint32_t vlo = (uint32_t)(int64_t)EJSVAL_TO_NUMBER(args[2]); return Value_new (llvm::Constant::getIntegerValue(ty, llvm::APInt(ty->getPrimitiveSizeInBits(), (int64_t)((vhi << 32) | vlo)))); } else @@ -90,6 +96,8 @@ namespace ejsllvm { _ejs_gc_add_root (&_ejs_Constant_prototype); _ejs_Constant_prototype = _ejs_object_create(_ejs_Object_prototype); + _ejs_gc_add_root (&_ejs_Constant); + _ejs_Constant = _ejs_function_new_utf8_with_proto (_ejs_null, "LLVMConstant", (EJSClosureFunc)Constant_impl, _ejs_Constant_prototype); _ejs_object_setprop_utf8 (exports, "Constant", _ejs_Constant); diff --git a/ejs-llvm/constantarray.cpp b/ejs-llvm/constantarray.cpp index 4b5c5582..d82e9a74 100644 --- a/ejs-llvm/constantarray.cpp +++ b/ejs-llvm/constantarray.cpp @@ -39,6 +39,8 @@ namespace ejsllvm { _ejs_gc_add_root (&_ejs_ConstantArray_prototype); _ejs_ConstantArray_prototype = _ejs_object_create(_ejs_Object_prototype); + _ejs_gc_add_root (&_ejs_ConstantArray); + _ejs_ConstantArray = _ejs_function_new_utf8_with_proto (_ejs_null, "LLVMConstantArray", (EJSClosureFunc)ConstantArray_impl, _ejs_ConstantArray_prototype); _ejs_object_setprop_utf8 (exports, "ConstantArray", _ejs_ConstantArray); diff --git a/ejs-llvm/constantfp.cpp b/ejs-llvm/constantfp.cpp index cf649848..abd781ad 100644 --- a/ejs-llvm/constantfp.cpp +++ b/ejs-llvm/constantfp.cpp @@ -32,6 +32,8 @@ namespace ejsllvm { _ejs_gc_add_root (&_ejs_ConstantFP_prototype); _ejs_ConstantFP_prototype = _ejs_object_create(_ejs_Object_prototype); + _ejs_gc_add_root (&_ejs_ConstantFP); + _ejs_ConstantFP = _ejs_function_new_utf8_with_proto (_ejs_null, "LLVMConstantFP", (EJSClosureFunc)ConstantFP_impl, _ejs_ConstantFP_prototype); _ejs_object_setprop_utf8 (exports, "ConstantFP", _ejs_ConstantFP); diff --git a/ejs-llvm/dibuilder.cpp b/ejs-llvm/dibuilder.cpp index 9e5308d3..6a34c33a 100644 --- a/ejs-llvm/dibuilder.cpp +++ b/ejs-llvm/dibuilder.cpp @@ -149,6 +149,8 @@ namespace ejsllvm { _ejs_gc_add_root (&_ejs_DIBuilder_prototype); _ejs_DIBuilder_prototype = _ejs_object_create(_ejs_Object_prototype); + _ejs_gc_add_root (&_ejs_DIBuilder); + _ejs_DIBuilder = _ejs_function_new_utf8_with_proto (_ejs_null, "LLVMDIBuilder", (EJSClosureFunc)DIBuilder_impl, _ejs_DIBuilder_prototype); _ejs_object_setprop_utf8 (exports, "DIBuilder", _ejs_DIBuilder); @@ -219,6 +221,8 @@ namespace ejsllvm { _ejs_gc_add_root (&_ejs_DIScope_prototype); _ejs_DIScope_prototype = _ejs_object_create(_ejs_Object_prototype); + _ejs_gc_add_root (&_ejs_DIScope); + _ejs_DIScope = _ejs_function_new_utf8_with_proto (_ejs_null, "LLVMDIScope", (EJSClosureFunc)DIScope_impl, _ejs_DIScope_prototype); _ejs_object_setprop_utf8 (exports, "DIScope", _ejs_DIScope); @@ -271,6 +275,8 @@ namespace ejsllvm { _ejs_gc_add_root (&_ejs_DISubprogram_prototype); _ejs_DISubprogram_prototype = _ejs_object_create(_ejs_Object_prototype); + _ejs_gc_add_root (&_ejs_DISubprogram); + _ejs_DISubprogram = _ejs_function_new_utf8_with_proto (_ejs_null, "LLVMDISubprogram", (EJSClosureFunc)DISubprogram_impl, _ejs_DISubprogram_prototype); _ejs_object_setprop_utf8 (exports, "DISubprogram", _ejs_DISubprogram); @@ -325,6 +331,8 @@ namespace ejsllvm { _ejs_gc_add_root (&_ejs_DIFile_prototype); _ejs_DIFile_prototype = _ejs_object_create(_ejs_Object_prototype); + _ejs_gc_add_root (&_ejs_DIFile); + _ejs_DIFile = _ejs_function_new_utf8_with_proto (_ejs_null, "LLVMDIFile", (EJSClosureFunc)DIFile_impl, _ejs_DIFile_prototype); _ejs_object_setprop_utf8 (exports, "DIFile", _ejs_DIFile); @@ -373,6 +381,8 @@ namespace ejsllvm { _ejs_gc_add_root (&_ejs_DICompileUnit_prototype); _ejs_DICompileUnit_prototype = _ejs_object_create(_ejs_Object_prototype); + _ejs_gc_add_root (&_ejs_DICompileUnit); + _ejs_DICompileUnit = _ejs_function_new_utf8_with_proto (_ejs_null, "LLVMDICompileUnit", (EJSClosureFunc)DICompileUnit_impl, _ejs_DICompileUnit_prototype); _ejs_object_setprop_utf8 (exports, "DICompileUnit", _ejs_DICompileUnit); @@ -421,6 +431,8 @@ namespace ejsllvm { _ejs_gc_add_root (&_ejs_DILexicalBlock_prototype); _ejs_DILexicalBlock_prototype = _ejs_object_create(_ejs_Object_prototype); + _ejs_gc_add_root (&_ejs_DILexicalBlock); + _ejs_DILexicalBlock = _ejs_function_new_utf8_with_proto (_ejs_null, "LLVMDILexicalBlock", (EJSClosureFunc)DILexicalBlock_impl, _ejs_DILexicalBlock_prototype); _ejs_object_setprop_utf8 (exports, "DILexicalBlock", _ejs_DILexicalBlock); @@ -483,6 +495,8 @@ namespace ejsllvm { _ejs_gc_add_root (&_ejs_DebugLoc_prototype); _ejs_DebugLoc_prototype = _ejs_object_create(_ejs_Object_prototype); + _ejs_gc_add_root (&_ejs_DebugLoc); + _ejs_DebugLoc = _ejs_function_new_utf8_with_proto (_ejs_null, "LLVMDebugLoc", (EJSClosureFunc)DebugLoc_impl, _ejs_DebugLoc_prototype); _ejs_object_setprop_utf8 (exports, "DebugLoc", _ejs_DebugLoc); diff --git a/ejs-llvm/ejs-llvm-atoms.h b/ejs-llvm/ejs-llvm-atoms.h index 3aede838..b5e473a8 100644 --- a/ejs-llvm/ejs-llvm-atoms.h +++ b/ejs-llvm/ejs-llvm-atoms.h @@ -24,6 +24,7 @@ EJS_ATOM(getParamType) EJS_ATOM(setInitializer) EJS_ATOM(setCleanup) EJS_ATOM(addClause) +EJS_ATOM(addIncoming) EJS_ATOM(getGlobalVariable) EJS_ATOM(getOrInsertIntrinsic) EJS_ATOM(getOrInsertFunction) @@ -51,6 +52,10 @@ EJS_ATOM(createFPCast) EJS_ATOM(createCall) EJS_ATOM(createInvoke) EJS_ATOM(createFAdd) +EJS_ATOM(createFSub) +EJS_ATOM(createFMul) +EJS_ATOM(createFDiv) +EJS_ATOM(createFCmpOLT) EJS_ATOM(createAlloca) EJS_ATOM(createLoad) EJS_ATOM(createStore) diff --git a/ejs-llvm/ejs-llvm.cpp b/ejs-llvm/ejs-llvm.cpp index 237a1de3..0c88c7cc 100644 --- a/ejs-llvm/ejs-llvm.cpp +++ b/ejs-llvm/ejs-llvm.cpp @@ -21,6 +21,7 @@ #include "allocainst.h" #include "loadinst.h" #include "landingpad.h" +#include "phinode.h" #include "dibuilder.h" namespace ejsllvm { @@ -66,6 +67,7 @@ _ejs_llvm_init (ejsval global) ConstantFP_init (global); Switch_init (global); LandingPad_init (global); + PhiNode_init (global); AllocaInst_init (global); LoadInst_init (global); #if notyet diff --git a/ejs-llvm/ejs-llvm.ejs.in b/ejs-llvm/ejs-llvm.ejs.in index 04b57bf0..2b20370f 100644 --- a/ejs-llvm/ejs-llvm.ejs.in +++ b/ejs-llvm/ejs-llvm.ejs.in @@ -1,12 +1,11 @@ { "ejs_version": "@EJS_VERSION@", - "module_name": "llvm", - "init_function": "_ejs_llvm_init", + "init_function": "_ejs_llvm_init", "link_flags": "@LLVM_LINK_FLAGS@", "module_file": "libejsllvm-module.a", - "module_version": "0.0.0-alpha1", - - "exports": [ "IRBuilder" ] + "exports": [ + "IRBuilder" + ] } diff --git a/ejs-llvm/functiontype.cpp b/ejs-llvm/functiontype.cpp index cfc257a5..c544fbe2 100644 --- a/ejs-llvm/functiontype.cpp +++ b/ejs-llvm/functiontype.cpp @@ -91,6 +91,8 @@ namespace ejsllvm { _ejs_gc_add_root (&_ejs_FunctionType_prototype); _ejs_FunctionType_prototype = _ejs_object_create (Type_get_prototype()); + _ejs_gc_add_root (&_ejs_FunctionType); + _ejs_FunctionType = _ejs_function_new_utf8_with_proto (_ejs_null, "LLVMFunctionType", (EJSClosureFunc)FunctionType_impl, _ejs_FunctionType_prototype); _ejs_object_setprop_utf8 (exports, "FunctionType", _ejs_FunctionType); diff --git a/ejs-llvm/irbuilder.cpp b/ejs-llvm/irbuilder.cpp index 07fb36b6..b91fa96b 100644 --- a/ejs-llvm/irbuilder.cpp +++ b/ejs-llvm/irbuilder.cpp @@ -13,6 +13,7 @@ #include "type.h" #include "value.h" #include "landingpad.h" +#include "phinode.h" #include "switch.h" #include "callinvoke.h" #include "basicblock.h" @@ -125,6 +126,38 @@ namespace ejsllvm { return Value_new (_llvm_builder.CreateFAdd(left, right, name)); } + static EJS_NATIVE_FUNC(IRBuilder_createFSub) { + REQ_LLVM_VAL_ARG(0, left); + REQ_LLVM_VAL_ARG(1, right); + FALLBACK_EMPTY_UTF8_ARG(2, name); + + return Value_new (_llvm_builder.CreateFSub(left, right, name)); + } + + static EJS_NATIVE_FUNC(IRBuilder_createFMul) { + REQ_LLVM_VAL_ARG(0, left); + REQ_LLVM_VAL_ARG(1, right); + FALLBACK_EMPTY_UTF8_ARG(2, name); + + return Value_new (_llvm_builder.CreateFMul(left, right, name)); + } + + static EJS_NATIVE_FUNC(IRBuilder_createFDiv) { + REQ_LLVM_VAL_ARG(0, left); + REQ_LLVM_VAL_ARG(1, right); + FALLBACK_EMPTY_UTF8_ARG(2, name); + + return Value_new (_llvm_builder.CreateFDiv(left, right, name)); + } + + static EJS_NATIVE_FUNC(IRBuilder_createFCmpOLT) { + REQ_LLVM_VAL_ARG(0, left); + REQ_LLVM_VAL_ARG(1, right); + FALLBACK_EMPTY_UTF8_ARG(2, name); + + return Value_new (_llvm_builder.CreateFCmpOLT(left, right, name)); + } + static EJS_NATIVE_FUNC(IRBuilder_createAlloca) { REQ_LLVM_TYPE_ARG(0, ty); FALLBACK_EMPTY_UTF8_ARG(1, name); @@ -258,23 +291,20 @@ namespace ejsllvm { } static EJS_NATIVE_FUNC(IRBuilder_createPhi) { - EJS_NOT_IMPLEMENTED(); -#if notyet REQ_LLVM_TYPE_ARG(0, ty); REQ_INT_ARG(1, incoming_values); FALLBACK_EMPTY_UTF8_ARG(2, name); - ejsval rv = Value_new (_llvm_builder.CreatePHI(ty, incoming_values, name)); - free (name); - return rv; -#endif + return PhiNode_new (_llvm_builder.CreatePHI(ty, incoming_values, name)); } static EJS_NATIVE_FUNC(IRBuilder_createGlobalStringPtr) { REQ_UTF8_ARG(0, val); FALLBACK_EMPTY_UTF8_ARG(1, name); - return Value_new (_llvm_builder.CreateGlobalStringPtr(val, name)); + // CreateGlobalStringPtr was removed in llvm 20; CreateGlobalString + // is identical under opaque pointers + return Value_new (_llvm_builder.CreateGlobalString(val, name)); } static EJS_NATIVE_FUNC(IRBuilder_createUnreachable) { @@ -392,6 +422,8 @@ namespace ejsllvm { _ejs_gc_add_root (&_ejs_IRBuilder_prototype); _ejs_IRBuilder_prototype = _ejs_object_create(_ejs_Object_prototype); + _ejs_gc_add_root (&_ejs_IRBuilder); + _ejs_IRBuilder = _ejs_function_new_utf8_with_proto (_ejs_null, "LLVMIRBuilder", (EJSClosureFunc)IRBuilder_impl, _ejs_IRBuilder_prototype); _ejs_object_setprop_utf8 (exports, "IRBuilder", _ejs_IRBuilder); @@ -408,6 +440,10 @@ namespace ejsllvm { OBJ_METHOD(createCall); OBJ_METHOD(createInvoke); OBJ_METHOD(createFAdd); + OBJ_METHOD(createFSub); + OBJ_METHOD(createFMul); + OBJ_METHOD(createFDiv); + OBJ_METHOD(createFCmpOLT); OBJ_METHOD(createAlloca); OBJ_METHOD(createLoad); OBJ_METHOD(createStore); diff --git a/ejs-llvm/landingpad.cpp b/ejs-llvm/landingpad.cpp index 7ac30db6..3dc0c287 100644 --- a/ejs-llvm/landingpad.cpp +++ b/ejs-llvm/landingpad.cpp @@ -94,6 +94,8 @@ namespace ejsllvm { _ejs_gc_add_root (&_ejs_LandingPad_prototype); _ejs_LandingPad_prototype = _ejs_object_new(_ejs_Object_prototype, &_ejs_LandingPad_specops); + _ejs_gc_add_root (&_ejs_LandingPad); + _ejs_LandingPad = _ejs_function_new_utf8_with_proto (_ejs_null, "LLVMLandingPad", (EJSClosureFunc)LandingPad_impl, _ejs_LandingPad_prototype); _ejs_object_setprop_utf8 (exports, "LandingPad", _ejs_LandingPad); diff --git a/ejs-llvm/loadinst.cpp b/ejs-llvm/loadinst.cpp index afcf15ac..78e6eb66 100644 --- a/ejs-llvm/loadinst.cpp +++ b/ejs-llvm/loadinst.cpp @@ -77,6 +77,8 @@ namespace ejsllvm { _ejs_gc_add_root (&_ejs_LoadInst_prototype); _ejs_LoadInst_prototype = _ejs_object_new(_ejs_Object_prototype, &_ejs_LoadInst_specops); + _ejs_gc_add_root (&_ejs_LoadInst); + _ejs_LoadInst = _ejs_function_new_utf8_with_proto (_ejs_null, "LLVMLoadInst", (EJSClosureFunc)LoadInst_impl, _ejs_LoadInst_prototype); _ejs_object_setprop_utf8 (exports, "LoadInst", _ejs_LoadInst); diff --git a/ejs-llvm/module.cpp b/ejs-llvm/module.cpp index e0994ab2..6ae05730 100644 --- a/ejs-llvm/module.cpp +++ b/ejs-llvm/module.cpp @@ -88,9 +88,10 @@ namespace ejsllvm { } #if false - llvm::Function* f = llvm::Intrinsic::getDeclaration (module->llvm_module, intrinsic_id, param_types); + llvm::Function* f = llvm::Intrinsic::getOrInsertDeclaration (module->llvm_module, intrinsic_id, param_types); #else - llvm::Function* f = llvm::Intrinsic::getDeclaration (module->llvm_module, intrinsic_id); + // renamed from getDeclaration in llvm 20 + llvm::Function* f = llvm::Intrinsic::getOrInsertDeclaration (module->llvm_module, intrinsic_id); #endif return Function_new (f); @@ -234,7 +235,8 @@ namespace ejsllvm { REQ_UTF8_ARG(0, triple); - module->llvm_module->setTargetTriple (triple); + // setTargetTriple takes an llvm::Triple as of llvm 21 + module->llvm_module->setTargetTriple (llvm::Triple(triple)); return _ejs_undefined; } @@ -256,6 +258,8 @@ namespace ejsllvm { _ejs_gc_add_root (&_ejs_Module_prototype); _ejs_Module_prototype = _ejs_object_new(_ejs_Object_prototype, &_ejs_Module_specops); + _ejs_gc_add_root (&_ejs_Module); + _ejs_Module = _ejs_function_new_utf8_with_proto (_ejs_null, "LLVMModule", (EJSClosureFunc)Module_impl, _ejs_Module_prototype); _ejs_object_setprop_utf8 (exports, "Module", _ejs_Module); diff --git a/ejs-llvm/phinode.cpp b/ejs-llvm/phinode.cpp new file mode 100644 index 00000000..201e2096 --- /dev/null +++ b/ejs-llvm/phinode.cpp @@ -0,0 +1,101 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=cpp: + */ + +#include + +#include "ejs-llvm.h" +#include "ejs-object.h" +#include "ejs-function.h" +#include "ejs-string.h" + +#include "phinode.h" +#include "basicblock.h" +#include "value.h" + +namespace ejsllvm { + + /// phi nodes + + typedef struct { + /* object header */ + EJSObject obj; + + /* phi specific data */ + llvm::PHINode *llvm_phi; + } PhiNode; + + static EJSSpecOps _ejs_PhiNode_specops; + static ejsval _ejs_PhiNode_prototype EJSVAL_ALIGNMENT; + static ejsval _ejs_PhiNode EJSVAL_ALIGNMENT; + + static EJSObject* PhiNode_allocate() + { + return (EJSObject*)_ejs_gc_new(PhiNode); + } + + static EJS_NATIVE_FUNC(PhiNode_impl) { + EJS_NOT_IMPLEMENTED(); + } + + ejsval + PhiNode_new(llvm::PHINode* llvm_phi) + { + ejsval result = _ejs_object_new (_ejs_PhiNode_prototype, &_ejs_PhiNode_specops); + ((PhiNode*)EJSVAL_TO_OBJECT(result))->llvm_phi = llvm_phi; + return result; + } + + static EJS_NATIVE_FUNC(PhiNode_prototype_toString) { + std::string str; + llvm::raw_string_ostream str_ostream(str); + ((PhiNode*)EJSVAL_TO_OBJECT(*_this))->llvm_phi->print(str_ostream); + + return _ejs_string_new_utf8(trim(str_ostream.str()).c_str()); + } + + static EJS_NATIVE_FUNC(PhiNode_prototype_dump) { + // ((PhiNode*)EJSVAL_TO_OBJECT(*_this))->llvm_phi->dump(); + return _ejs_undefined; + } + + static EJS_NATIVE_FUNC(PhiNode_prototype_addIncoming) { + PhiNode *phi = ((PhiNode*)EJSVAL_TO_OBJECT(*_this)); + REQ_LLVM_VAL_ARG(0, incoming_val); + REQ_LLVM_BB_ARG(1, incoming_bb); + phi->llvm_phi->addIncoming(incoming_val, incoming_bb); + return _ejs_undefined; + } + + llvm::PHINode* + PhiNode_GetLLVMObj(ejsval val) + { + if (EJSVAL_IS_NULL(val)) return NULL; + return ((PhiNode*)EJSVAL_TO_OBJECT(val))->llvm_phi; + } + + void + PhiNode_init (ejsval exports) + { + _ejs_PhiNode_specops = _ejs_Object_specops; + _ejs_PhiNode_specops.class_name = "LLVMPhiNode"; + _ejs_PhiNode_specops.Allocate = PhiNode_allocate; + + _ejs_gc_add_root (&_ejs_PhiNode_prototype); + _ejs_PhiNode_prototype = _ejs_object_new(_ejs_Object_prototype, &_ejs_PhiNode_specops); + + _ejs_gc_add_root (&_ejs_PhiNode); + + _ejs_PhiNode = _ejs_function_new_utf8_with_proto (_ejs_null, "LLVMPhiNode", (EJSClosureFunc)PhiNode_impl, _ejs_PhiNode_prototype); + + _ejs_object_setprop_utf8 (exports, "PhiNode", _ejs_PhiNode); + +#define PROTO_METHOD(x) EJS_INSTALL_ATOM_FUNCTION(_ejs_PhiNode_prototype, x, PhiNode_prototype_##x) + + PROTO_METHOD(dump); + PROTO_METHOD(toString); + PROTO_METHOD(addIncoming); + +#undef PROTO_METHOD + } +}; diff --git a/ejs-llvm/phinode.h b/ejs-llvm/phinode.h new file mode 100644 index 00000000..8ae1bc38 --- /dev/null +++ b/ejs-llvm/phinode.h @@ -0,0 +1,14 @@ +#ifndef EJS_LLVM_PHINODE_H +#define EJS_LLVM_PHINODE_H + +#include "ejs-llvm.h" + +namespace ejsllvm { + extern void PhiNode_init (ejsval exports); + + ejsval PhiNode_new(llvm::PHINode* llvm_phi); + + extern llvm::PHINode* PhiNode_GetLLVMObj(ejsval val); +}; + +#endif /* EJS_LLVM_PHINODE_H */ diff --git a/ejs-llvm/structtype.cpp b/ejs-llvm/structtype.cpp index e30acb15..fbdba65b 100644 --- a/ejs-llvm/structtype.cpp +++ b/ejs-llvm/structtype.cpp @@ -78,7 +78,9 @@ namespace ejsllvm { void StructType_init (ejsval exports) { + _ejs_gc_add_root (&_ejs_StructType_prototype); _ejs_StructType_prototype = _ejs_object_create (Type_get_prototype()); + _ejs_gc_add_root (&_ejs_StructType); _ejs_StructType = _ejs_function_new_utf8_with_proto (_ejs_null, "LLVMStructType", (EJSClosureFunc)StructType_impl, _ejs_StructType_prototype); _ejs_object_setprop_utf8 (exports, "StructType", _ejs_StructType); diff --git a/ejs-llvm/switch.cpp b/ejs-llvm/switch.cpp index a2bb6450..807c4e13 100644 --- a/ejs-llvm/switch.cpp +++ b/ejs-llvm/switch.cpp @@ -89,6 +89,8 @@ namespace ejsllvm { _ejs_gc_add_root (&_ejs_Switch_prototype); _ejs_Switch_prototype = _ejs_object_new(_ejs_Object_prototype, &_ejs_Switch_specops); + _ejs_gc_add_root (&_ejs_Switch); + _ejs_Switch = _ejs_function_new_utf8_with_proto (_ejs_null, "LLVMSwitch", (EJSClosureFunc)Switch_impl, _ejs_Switch_prototype); _ejs_object_setprop_utf8 (exports, "Switch", _ejs_Switch); diff --git a/ejs-llvm/type.cpp b/ejs-llvm/type.cpp index 6fb1f783..bf7842b6 100644 --- a/ejs-llvm/type.cpp +++ b/ejs-llvm/type.cpp @@ -55,7 +55,9 @@ namespace ejsllvm { #undef LLVM_TYPE_METHOD static EJS_NATIVE_FUNC(Type_prototype_pointerTo) { - return Type_new(((Type*)EJSVAL_TO_OBJECT(*_this))->type->getPointerTo()); + // Type::getPointerTo was removed in llvm 21; all pointers are opaque + llvm::Type* ty = ((Type*)EJSVAL_TO_OBJECT(*_this))->type; + return Type_new(llvm::PointerType::getUnqual(ty->getContext())); } static EJS_NATIVE_FUNC(Type_prototype_isVoid) { @@ -97,6 +99,8 @@ namespace ejsllvm { _ejs_gc_add_root (&_ejs_Type_prototype); _ejs_Type_prototype = _ejs_object_create(_ejs_Object_prototype); + _ejs_gc_add_root (&_ejs_Type); + _ejs_Type = _ejs_function_new_utf8_with_proto (_ejs_null, "LLVMType", (EJSClosureFunc)Type_impl, _ejs_Type_prototype); _ejs_object_setprop_utf8 (exports, "Type", _ejs_Type); diff --git a/ejs-llvm/value.cpp b/ejs-llvm/value.cpp index c0be170b..1ff1834e 100644 --- a/ejs-llvm/value.cpp +++ b/ejs-llvm/value.cpp @@ -78,6 +78,8 @@ namespace ejsllvm { _ejs_gc_add_root (&_ejs_Value_prototype); _ejs_Value_prototype = _ejs_object_new(_ejs_Object_prototype, &_ejs_Object_specops); + _ejs_gc_add_root (&_ejs_Value); + _ejs_Value = _ejs_function_new_utf8_with_proto (_ejs_null, "LLVMValue", (EJSClosureFunc)Value_impl, _ejs_Value_prototype); _ejs_object_setprop_utf8 (exports, "Value", _ejs_Value); diff --git a/external-deps/BUCK b/external-deps/BUCK new file mode 100644 index 00000000..84b08538 --- /dev/null +++ b/external-deps/BUCK @@ -0,0 +1,103 @@ +load("//:defs.bzl", "GNU_TRIPLE") + +# --------------------------------------------------------------------------- +# parson: compiled directly into libecho (see //runtime:echo), we just +# export the source and a header mapped to the repo-relative include path +# the runtime sources use. +# --------------------------------------------------------------------------- + +export_file( + name = "parson.c", + src = "parson/parson.c", + visibility = ["PUBLIC"], +) + +cxx_library( + name = "parson-headers", + header_namespace = "", + exported_headers = { + "external-deps/parson/parson.h": "parson/parson.h", + # parson.c itself includes it unqualified + "parson.h": "parson/parson.h", + }, + visibility = ["PUBLIC"], +) + +# --------------------------------------------------------------------------- +# pcre: autotools build. Produces libpcre16.a plus the configure-generated +# pcre.h that runtime/ejs-regexp.c includes as "external-deps/pcre/pcre.h". +# --------------------------------------------------------------------------- + +genrule( + name = "pcre-build", + srcs = glob(["pcre/**"]), + outs = { + "lib": ["libpcre16.a"], + "header": ["pcre.h"], + }, + default_outs = ["libpcre16.a"], + cmd = 'set -e; BUILD="$TMP/pcre-build"; mkdir -p "$BUILD"; ' + + 'SRC="$PWD/$SRCDIR/pcre"; ' + + '(cd "$BUILD" && "$SRC/configure" --build=' + GNU_TRIPLE + + ' --enable-pcre16 --enable-utf --disable-cpp >configure.log 2>&1 && ' + + 'make pcre_chartables.c libpcre16.la >build.log 2>&1) || { cat "$BUILD"/*.log; exit 1; }; ' + + 'cp "$BUILD/.libs/libpcre16.a" "$OUT/libpcre16.a"; ' + + 'cp "$BUILD/pcre.h" "$OUT/pcre.h"', + visibility = ["PUBLIC"], +) + +cxx_library( + name = "pcre-headers", + header_namespace = "", + exported_headers = { + "external-deps/pcre/pcre.h": ":pcre-build[header]", + }, + visibility = ["PUBLIC"], +) + +# --------------------------------------------------------------------------- +# double-conversion: cmake build for the static lib; headers come straight +# from the source tree, remapped to the repo-relative include path +# runtime/ejs-dtoa.cpp uses. +# --------------------------------------------------------------------------- + +genrule( + name = "double-conversion-build", + srcs = glob(["double-conversion/**"]), + out = "libdouble-conversion.a", + cmd = 'set -e; BUILD="$TMP/dc-build"; mkdir -p "$BUILD"; ' + + 'SRC="$PWD/$SRCDIR/double-conversion"; ' + + '(cd "$BUILD" && cmake -DCMAKE_POLICY_VERSION_MINIMUM=3.5 -DCMAKE_BUILD_TYPE=Release "$SRC" >cmake.log 2>&1 && ' + + 'make >build.log 2>&1) || { cat "$BUILD"/*.log; exit 1; }; ' + + 'cp "$BUILD/double-conversion/libdouble-conversion.a" "$OUT"', + visibility = ["PUBLIC"], +) + +cxx_library( + name = "double-conversion-headers", + header_namespace = "", + exported_headers = { + "external-deps/double-conversion/" + h.removeprefix("double-conversion/double-conversion/"): h + for h in glob(["double-conversion/double-conversion/*.h"]) + }, + visibility = ["PUBLIC"], +) + +# --------------------------------------------------------------------------- +# JS sources for the compiler itself (esprima & friends), staged into the +# --srcdir tree and fed through babel for the stage0 (node-hosted) compiler. +# --------------------------------------------------------------------------- + +filegroup( + name = "compiler-js", + srcs = glob([ + "esprima/esprima-es6.js", + "esprima/esprima-es6.d.ts", + "escodegen/escodegen-es6.js", + "escodegen/escodegen-es6.d.ts", + "estraverse/estraverse-es6.js", + "esutils/esutils-es6.js", + "esutils/lib/*.js", + ]), + visibility = ["PUBLIC"], +) diff --git a/external-deps/Makefile b/external-deps/Makefile deleted file mode 100644 index c9451ffe..00000000 --- a/external-deps/Makefile +++ /dev/null @@ -1,161 +0,0 @@ -TOP=.. -include $(TOP)/build/config.mk - -LLVM_CXXFLAGS="`$LLVM_CONFIG --cxxflags` -fno-rtti" -LLVM_LDFLAGS=`$LLVM_CONFIG --ldflags` -LLVM_LIBS=`$LLVM_CONFIG --libs core bitwriter jit x86codegen` -LLVM_LIBS:="$LLVM_LDFLAGS $LLVM_LIBS -lstdc++" -LLVM_CONFIGURE_ARGS=--disable-jit --enable-static --enable-optimized --disable-assertions - -PCRE_CONFIGURE_ARGS=--enable-pcre16 --enable-utf --disable-cpp - -CFLAGS=-I$(TOP)/runtime - -all-local:: build-pcre build-double-conversion - -clean-local:: clean-pcre clean-double-conversion - -install-local:: install-pcre install-double-conversion - -ifeq ($(HOST_OS),linux) -_TARGETS=linux -else -ifneq ($(CIRCLE_BUILD_NUM),) -_TARGETS=osx -else -_TARGETS=iossim iosdev osx -endif -endif - -build-double-conversion: $(_TARGETS:%=build-double-conversion-%) - -clean-double-conversion: $(_TARGETS:%=clean-double-conversion-%) - -.stamp-configure-double-conversion-linux: double-conversion/CMakeLists.txt - @$(MKDIR) double-conversion-linux - (cd double-conversion-linux && cmake ../double-conversion) && touch $@ - -.stamp-build-double-conversion-linux: .stamp-configure-double-conversion-linux - $(MAKE) -C double-conversion-linux && touch $@ - -.stamp-configure-double-conversion-osx: double-conversion/CMakeLists.txt - @$(MKDIR) double-conversion-osx - (cd double-conversion-osx && cmake ../double-conversion) && touch $@ - -.stamp-configure-double-conversion-iossim: double-conversion/CMakeLists.txt - @$(MKDIR) double-conversion-iossim - (cd double-conversion-iossim && \ - cmake ../double-conversion -DCMAKE_TOOLCHAIN_FILE=../../build/iOS.cmake -DIOS_PLATFORM=SIMULATOR64 -DMIN_IOS_VERSION=$(MIN_IOS_VERSION) -DIOS_SYSROOT=$(IOSSIM_SYSROOT)) && touch $@ - -.stamp-configure-double-conversion-iosdev: double-conversion/CMakeLists.txt - @$(MKDIR) double-conversion-iosdev - (cd double-conversion-iosdev && \ - cmake ../double-conversion -DCMAKE_TOOLCHAIN_FILE=../../build/iOS.cmake -DIOS_PLATFORM=OS -DMIN_IOS_VERSION=$(MIN_IOS_VERSION) -DIOS_SYSROOT=$(IOSDEV_SYSROOT)) && touch $@ - -.stamp-build-double-conversion-osx: .stamp-configure-double-conversion-osx - $(MAKE) -C double-conversion-osx && touch $@ - -.stamp-build-double-conversion-iossim: .stamp-configure-double-conversion-iossim - $(MAKE) -C double-conversion-iossim && touch $@ - -.stamp-build-double-conversion-iosdev: .stamp-configure-double-conversion-iosdev - $(MAKE) -C double-conversion-iosdev && touch $@ - - -build-double-conversion-linux: .stamp-build-double-conversion-linux -build-double-conversion-osx: .stamp-build-double-conversion-osx -build-double-conversion-iossim: .stamp-build-double-conversion-iossim -build-double-conversion-iosdev: .stamp-build-double-conversion-iosdev - - -clean-double-conversion-iossim: - -@test -d double-conversion-iossim && $(MAKE) -C double-conversion-iossim clean - @rm -f .stamp-build-double-conversion-iossim - -clean-double-conversion-iosdev: - -@test -d double-conversion-iosdev && $(MAKE) -C double-conversion-iosdev clean - @rm -f .stamp-build-double-conversion-iosdev - -clean-double-conversion-osx: - -@test -d double-conversion-osx && $(MAKE) -C double-conversion-osx clean - @rm -f .stamp-build-double-conversion-osx - -clean-double-conversion-linux: - -@test -d double-conversion-linux && $(MAKE) -C double-conversion-linux clean - @rm -f .stamp-build-double-conversion-linux - -build-pcre: $(_TARGETS:%=build-pcre-%) - -clean-pcre: $(_TARGETS:%=clean-pcre-%) - -.stamp-configure-pcre-linux: pcre/configure - @$(MKDIR) pcre-linux - (cd pcre-linux && \ - ../pcre/configure $(PCRE_CONFIGURE_ARGS)) && touch $@ - -.stamp-configure-pcre-osx: pcre/configure - @$(MKDIR) pcre-osx - (cd pcre-osx && \ - ../pcre/configure $(PCRE_CONFIGURE_ARGS)) && touch $@ - -.stamp-configure-pcre-iossim: pcre/configure - @$(MKDIR) pcre-iossim - (cd pcre-iossim && \ - PATH=$(IOSSIM_ROOT)/usr/bin:$$PATH \ - CC="clang $(IOSSIM_ARCH) $(IOSSIM_ARCH_FLAGS) -miphoneos-version-min=$(MIN_IOS_VERSION) -isysroot $(IOSSIM_SYSROOT)" \ - CXX="clang++ $(IOSSIM_ARCH) $(IOSSIM_ARCH_FLAGS) -miphoneos-version-min=$(MIN_IOS_VERSION) -isysroot $(IOSSIM_SYSROOT)" \ - LD="clang" \ - AS="$(IOSSIM_ROOT)/usr/bin/as" \ - ../pcre/configure --host=$(IOSSIM_TRIPLE) $(PCRE_CONFIGURE_ARGS)) && touch $@ - -.stamp-configure-pcre-iosdev: pcre/configure - @$(MKDIR) pcre-iosdev - (cd pcre-iosdev && \ - PATH=$(IOSDEV_ROOT)/usr/bin:$$PATH \ - CC="clang $(IOSDEV_ARCH) $(IOSDEV_ARCH_FLAGS) -miphoneos-version-min=$(MIN_IOS_VERSION) -isysroot $(IOSDEV_SYSROOT)" \ - CXX="clang++ $(IOSDEV_ARCH) $(IOSDEV_ARCH_FLAGS) -miphoneos-version-min=$(MIN_IOS_VERSION) -isysroot $(IOSDEV_SYSROOT)" \ - LD="clang" \ - AS="$(IOSDEV_ROOT)/usr/bin/as" \ - ../pcre/configure --host=$(IOSDEV_TRIPLE) $(PCRE_CONFIGURE_ARGS)) && touch $@ - -.stamp-build-pcre-linux: .stamp-configure-pcre-linux - $(MAKE) -C pcre-linux pcre_chartables.c libpcre16.la && touch $@ - -.stamp-build-pcre-osx: .stamp-configure-pcre-osx - $(MAKE) -C pcre-osx pcre_chartables.c libpcre16.la && touch $@ - -.stamp-build-pcre-iossim: .stamp-configure-pcre-iossim - $(MAKE) -C pcre-iossim pcre_chartables.c libpcre16.la && touch $@ - -.stamp-build-pcre-iosdev: .stamp-configure-pcre-iosdev - $(MAKE) -C pcre-iosdev pcre_chartables.c libpcre16.la && touch $@ - - - -build-pcre-linux: .stamp-build-pcre-linux -build-pcre-osx: .stamp-build-pcre-osx -build-pcre-iossim: .stamp-build-pcre-iossim -build-pcre-iosdev: .stamp-build-pcre-iosdev - - -install-pcre-linux: build-pcre-linux - @$(MKDIR) $(archlibdir) - $(INSTALL) -c pcre-linux/.libs/libpcre16.a $(archlibdir) - -clean-pcre-iossim: - -@test -d pcre-iossim && $(MAKE) -C pcre-iossim clean - @rm -f .stamp-build-pcre-iossim - -clean-pcre-iosdev: - -@test -d pcre-iosdev && $(MAKE) -C pcre-iosdev clean - @rm -f .stamp-build-pcre-iosdev - -clean-pcre-osx: - -@test -d pcre-osx && $(MAKE) -C pcre-osx clean - @rm -f .stamp-build-pcre-osx - -clean-pcre-linux: - -@test -d pcre-linux && $(MAKE) -C pcre-linux clean - @rm -f .stamp-build-pcre-linux - -include $(TOP)/build/build.mk diff --git a/external-deps/echojs-maam b/external-deps/echojs-maam new file mode 160000 index 00000000..14320723 --- /dev/null +++ b/external-deps/echojs-maam @@ -0,0 +1 @@ +Subproject commit 143207230b194c183cab813a806f4484ab7d34d2 diff --git a/external-deps/escodegen b/external-deps/escodegen index d73e9e44..55ee4e89 160000 --- a/external-deps/escodegen +++ b/external-deps/escodegen @@ -1 +1 @@ -Subproject commit d73e9e44ebcd6c12042aa3a48ea97aee388bc8fd +Subproject commit 55ee4e89ff0e0ed0aae0c1480c919bfdb8391c0c diff --git a/external-deps/esprima b/external-deps/esprima index e4445c9c..25f43fc4 160000 --- a/external-deps/esprima +++ b/external-deps/esprima @@ -1 +1 @@ -Subproject commit e4445c9cc2530d672c4e9f68f5e2a53673b57af0 +Subproject commit 25f43fc4e54daa272ede68faf8d79056dd967a56 diff --git a/lib/BUCK b/lib/BUCK new file mode 100644 index 00000000..16c2e91b --- /dev/null +++ b/lib/BUCK @@ -0,0 +1,60 @@ +load("//:defs.bzl", "EJS_RUNLOOP_IMPL", "llvm_bin", "llvm_bindir", "llvm_suffix") + +genrule( + name = "host-config.js", + srcs = ["host-config.js.in"], + out = "host-config.js", + # LLVM_MAJOR: the major version of the toolchain this compiler is + # built against, baked in so the driver can refuse a mismatched + # opt/llc at runtime (the llvm@16-on-PATH miscompile was silent) + cmd = 'set -e; LLVM_MAJOR="`' + llvm_bin("llvm-config") + ' --version | cut -d. -f1`"; ' + + 'sed -e "s,@LLVM_SUFFIX@,' + llvm_suffix() + ',g" ' + + '-e "s,@LLVM_BINDIR@,' + llvm_bindir() + ',g" ' + + '-e "s,@LLVM_MAJOR@,$LLVM_MAJOR,g" ' + + '-e "s,@RUNLOOP_IMPL@,' + EJS_RUNLOOP_IMPL + ',g" ' + + "$SRCDIR/host-config.js.in > $OUT", + visibility = ["PUBLIC"], +) + +# The compiler as plain ES-module JS: .ts sources compiled through tsc +# (strict). Layout: $OUT/ejs-es6.js + $OUT/lib/**.js. This is what +# stage1+ self-compiles (via //:srcdir-tree) and what the CommonJS +# conversion below consumes for the node-hosted stage0. +genrule( + name = "tsjs", + srcs = glob( + [ + "*.js", + "*.ts", + "passes/*.js", + "passes/*.ts", + "eir/*.js", + "eir/*.ts", + ], + exclude = ["host-config.js"], + ) + [ + "buck-gen-tsjs.sh", + "//:ejs-es6.ts", + "//external-deps:compiler-js", + ], + out = "tsjs", + cmd = "bash $SRCDIR/buck-gen-tsjs.sh", + visibility = ["PUBLIC"], +) + +# The node-runnable (stage0) compiler: CommonJS (tsc-converted) +# equivalents of ejs-es6.js, lib/*.js and the esprima/escodegen/... +# support modules. Layout matches lib/generated/ from the Makefile +# build. +genrule( + name = "generated", + srcs = [ + "buck-gen-js.sh", + ":host-config.js", + ":tsjs", + "//external-deps:compiler-js", + ], + out = "generated", + cmd = "bash $SRCDIR/buck-gen-js.sh $(location :tsjs)", + visibility = ["PUBLIC"], +) diff --git a/lib/Makefile b/lib/Makefile deleted file mode 100644 index 18781be6..00000000 --- a/lib/Makefile +++ /dev/null @@ -1,119 +0,0 @@ -TOP=.. - -include $(TOP)/build/config.mk - -ES6_SOURCES= \ - abi.js \ - sret-abi.js \ - ast-builder.js \ - node-visitor.js \ - compiler.js \ - common-ids.js \ - closure-conversion.js \ - debug.js \ - echo-util.js \ - errors.js \ - optimizations.js \ - types.js \ - consts.js \ - exitable-scope.js \ - runtime.js \ - module-info.js \ - stack-es6.js \ - host-config.js \ - triple.js \ - passes/desugar-arguments.js \ - passes/desugar-arrow-functions.js \ - passes/desugar-classes.js \ - passes/desugar-defaults.js \ - passes/desugar-destructuring.js \ - passes/desugar-for-of.js \ - passes/desugar-generator-functions.js \ - passes/desugar-import-export.js \ - passes/desugar-let-loopvars.js \ - passes/desugar-metaproperties.js \ - passes/desugar-rest-parameters.js \ - passes/desugar-spread.js \ - passes/desugar-templates.js \ - passes/desugar-update-assignments.js \ - passes/eq-idioms.js \ - passes/func-decls-to-vars.js \ - passes/gather-imports.js \ - passes/hoist-func-decls.js \ - passes/hoist-vars.js \ - passes/iife-idioms.js \ - passes/lambda-lift.js \ - passes/name-anonymous-functions.js \ - passes/new-cc.js \ - passes/replace-unary-void.js \ - passes/substitute-variables.js - -DESTDIR = generated - -GENERATED_EXTERNAL_FILES = \ - $(DESTDIR)/ejs-es6.js \ - $(DESTDIR)/external-deps/esprima/esprima-es6.js \ - $(DESTDIR)/external-deps/escodegen/escodegen-es6.js \ - $(DESTDIR)/external-deps/estraverse/estraverse-es6.js \ - $(DESTDIR)/external-deps/esutils/esutils-es6.js \ - $(DESTDIR)/external-deps/esutils/lib/code.js \ - $(DESTDIR)/external-deps/esutils/lib/ast.js \ - $(DESTDIR)/external-deps/esutils/lib/keyword.js - -GENERATED_FILES=$(ES6_SOURCES:%.js=$(DESTDIR)/lib/%.js) - -all-local:: $(GENERATED_FILES) $(GENERATED_EXTERNAL_FILES) - -dist-local:: $(GENERATED_FILES) - -clean-local:: - rm -rf $(DESTDIR) host-config.js - -BABEL_SED_REPLACEMENTS= -e s,\"@llvm\",\"llvm\", \ - -e s,\'@llvm\',\'llvm\', \ - -e s,@node-compat/,, - -BABEL_ARGS=--config-file $(TOP)/.babelrc -BABEL=../node_modules/.bin/babel - -$(DESTDIR)/ejs-es6.js: ../ejs-es6.js - @echo [babel] $< - @mkdir -p `dirname $@` - @$(BABEL) $(BABEL_ARGS) -o "" $< | sed $(BABEL_SED_REPLACEMENTS) > $@ - -$(DESTDIR)/external-deps/esprima/esprima-es6.js: ../external-deps/esprima/esprima-es6.js - @echo [babel] $< - @mkdir -p `dirname $@` - @$(BABEL) $(BABEL_ARGS) -o "" $< | sed $(BABEL_SED_REPLACEMENTS) > $@ - -$(DESTDIR)/external-deps/escodegen/escodegen-es6.js: ../external-deps/escodegen/escodegen-es6.js - @echo [babel] $< - @mkdir -p `dirname $@` - @$(BABEL) $(BABEL_ARGS) -o "" $< | sed $(BABEL_SED_REPLACEMENTS) > $@ - -$(DESTDIR)/external-deps/estraverse/estraverse-es6.js: ../external-deps/estraverse/estraverse-es6.js - @echo [babel] $< - @mkdir -p `dirname $@` - @$(BABEL) $(BABEL_ARGS) -o "" $< | sed $(BABEL_SED_REPLACEMENTS) > $@ - -$(DESTDIR)/external-deps/esutils/esutils-es6.js: ../external-deps/esutils/esutils-es6.js - @echo [babel] $< - @mkdir -p `dirname $@` - @$(BABEL) $(BABEL_ARGS) -o "" $< | sed $(BABEL_SED_REPLACEMENTS) > $@ - -$(DESTDIR)/external-deps/esutils/lib/%.js: ../external-deps/esutils/lib/%.js - @echo [babel] $< - @mkdir -p `dirname $@` - @$(BABEL) $(BABEL_ARGS) -o "" $< | sed $(BABEL_SED_REPLACEMENTS) > $@ - -$(DESTDIR)/lib/%.js: %.js - @echo [babel] $< - @mkdir -p `dirname $@` - @$(BABEL) $(BABEL_ARGS) -o "" $< | sed $(BABEL_SED_REPLACEMENTS) > $@ - -%.js: %.js.in - @echo [gen] $@ && (cat $< | sed -e s,@LLVM_SUFFIX@,$(LLVM_SUFFIX),g -e s,@RUNLOOP_IMPL@,$(EJS_RUNLOOP_IMPL),g > $@) - -.PRECIOUS: host-config.js - -include $(TOP)/build/build.mk diff --git a/lib/abi.js b/lib/abi.js deleted file mode 100644 index 30eefef1..00000000 --- a/lib/abi.js +++ /dev/null @@ -1,76 +0,0 @@ -/* -*- Mode: js2; indent-tabs-mode: nil; tab-width: 4; js2-indent-offset: 4; js2-basic-offset: 4; -*- - * vim: set ts=4 sw=4 et tw=99 ft=js: - */ - -import * as llvm from "@llvm"; -import * as types from "./types"; - -let ir = llvm.IRBuilder; - -// our base ABI class assumes that there are no restrictions on -// EjsValue types, and that they can be passed by value and returned by -// value with no modification to signatures or callsites. -// -export class ABI { - constructor() { - this.ejs_return_type = types.EjsValue; - this.ejs_params = [ - { name: "%env", llvm_type: types.EjsValue }, // should be EjsClosureEnv - { name: "%this", llvm_type: types.EjsValue.pointerTo() }, - { name: "%argc", llvm_type: types.Int32 }, - { name: "%args", llvm_type: types.EjsValue.pointerTo() }, - { name: "%newTarget", llvm_type: types.EjsValue }, - ]; - this.env_param_index = 0; - this.this_param_index = 1; - this.argc_param_index = 2; - this.args_param_index = 3; - this.newTarget_param_index = 3; - } - - // this function c&p from LLVMIRVisitor below - createAlloca(func, type, name) { - let saved_insert_point = ir.getInsertBlock(); - ir.setInsertPointStartBB(func.entry_bb); - let alloca = ir.createAlloca(type, name); - - // if EjsValue was a pointer value we would be able to use an the llvm gcroot intrinsic here. but with the nan boxing - // we kinda lose out as the llvm IR code doesn't permit non-reference types to be gc roots. - // if type is types.EjsValue - // // EjsValues are rooted - // this.createCall this.llvm_intrinsics.gcroot(), [(ir.createPointerCast alloca, types.Int8Pointer.pointerTo(), 'rooted_alloca'), consts.Null types.Int8Pointer], '' - - ir.setInsertPoint(saved_insert_point); - return alloca; - } - forwardCalleeAttributes(fromCallee, toCall) { - if (fromCallee.doesNotThrow) toCall.setDoesNotThrow(); - if (fromCallee.doesNotAccessMemory) toCall.setDoesNotAccessMemory(); - if (!fromCallee.doesNotAccessMemory && fromCallee.onlyReadsMemory) - toCall.setOnlyReadsMemory(); - toCall._ejs_returns_ejsval_bool = fromCallee.returns_ejsval_bool; - } - - createCall(fromFunction, calleeType, callee, argv, callname) { - // XXX this is wrong currently (createCall/createInvoke must take another arg (the function type) - // new_llvm - return ir.createCall(calleeType, callee, argv, callname); - } - createInvoke(fromFunction, calleeType, callee, argv, normal_block, exc_block, callname) { - // XXX this is wrong currently (createCall/createInvoke must take another arg (the function type) - // new_llvm - return ir.createInvoke(calleeType, callee, argv, normal_block, exc_block, callname); - } - createRet(fromFunction, value) { - return ir.createRet(value); - } - createExternalFunction(inModule, name, ret_type, param_types) { - return inModule.getOrInsertExternalFunction(name, ret_type, param_types); - } - createFunction(inModule, name, ret_type, param_types) { - return inModule.getOrInsertFunction(name, ret_type, param_types); - } - createFunctionType(ret_type, param_types) { - return llvm.FunctionType.get(ret_type, param_types); - } -} diff --git a/lib/abi.ts b/lib/abi.ts new file mode 100644 index 00000000..c6004a73 --- /dev/null +++ b/lib/abi.ts @@ -0,0 +1,103 @@ +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: + */ + +import * as llvm from "@llvm"; +import * as types from "./types"; + +const ir = llvm.IRBuilder; + +export interface EjsParam { + name: string; + llvm_type: llvm.Type; +} + +// our base ABI class assumes that there are no restrictions on +// EjsValue types, and that they can be passed by value and returned by +// value with no modification to signatures or callsites. +// +export class ABI { + ejs_return_type: llvm.Type = types.EjsValue; + ejs_params: EjsParam[] = [ + { name: "%env", llvm_type: types.EjsValue }, // should be EjsClosureEnv + { name: "%this", llvm_type: types.EjsValue.pointerTo() }, + { name: "%argc", llvm_type: types.Int32 }, + { name: "%args", llvm_type: types.EjsValue.pointerTo() }, + { name: "%newTarget", llvm_type: types.EjsValue }, + ]; + env_param_index = 0; + this_param_index = 1; + argc_param_index = 2; + args_param_index = 3; + newTarget_param_index = 3; + + createAlloca(func: llvm.EjsFunction, type: llvm.Type, name: string): llvm.AllocaInst { + const saved_insert_point = ir.getInsertBlock(); + ir.setInsertPointStartBB(func.entry_bb!); + const alloca = ir.createAlloca(type, name); + + // if EjsValue was a pointer value we would be able to use the llvm + // gcroot intrinsic here. but with the nan boxing we kinda lose out + // as the llvm IR code doesn't permit non-reference types to be gc + // roots. + + ir.setInsertPoint(saved_insert_point); + return alloca; + } + + forwardCalleeAttributes(fromCallee: llvm.EjsFunction, toCall: llvm.CallInst): void { + if (fromCallee.doesNotThrow) toCall.setDoesNotThrow(); + if (fromCallee.doesNotAccessMemory) toCall.setDoesNotAccessMemory(); + if (!fromCallee.doesNotAccessMemory && fromCallee.onlyReadsMemory) + toCall.setOnlyReadsMemory(); + toCall._ejs_returns_ejsval_bool = fromCallee.returns_ejsval_bool; + } + + createCall( + fromFunction: llvm.EjsFunction, + calleeType: llvm.FunctionType, + callee: llvm.Value, + argv: llvm.Value[], + callname: string + ): llvm.Value { + return ir.createCall(calleeType, callee, argv, callname); + } + + createInvoke( + fromFunction: llvm.EjsFunction, + calleeType: llvm.FunctionType, + callee: llvm.Value, + argv: llvm.Value[], + normal_block: llvm.BasicBlock, + exc_block: llvm.BasicBlock, + callname: string + ): llvm.Value { + return ir.createInvoke(calleeType, callee, argv, normal_block, exc_block, callname); + } + + createRet(fromFunction: llvm.EjsFunction, value: llvm.Value): llvm.Value { + return ir.createRet(value); + } + + createExternalFunction( + inModule: llvm.Module, + name: string, + ret_type: llvm.Type, + param_types: llvm.Type[] + ): llvm.EjsFunction { + return inModule.getOrInsertExternalFunction(name, ret_type, param_types); + } + + createFunction( + inModule: llvm.Module, + name: string, + ret_type: llvm.Type, + param_types: llvm.Type[] + ): llvm.EjsFunction { + return inModule.getOrInsertFunction(name, ret_type, param_types); + } + + createFunctionType(ret_type: llvm.Type, param_types: llvm.Type[]): llvm.FunctionType { + return llvm.FunctionType.get(ret_type, param_types); + } +} diff --git a/lib/ast-builder.js b/lib/ast-builder.js deleted file mode 100644 index 523bfa41..00000000 --- a/lib/ast-builder.js +++ /dev/null @@ -1,363 +0,0 @@ -/* -*- Mode: js2; indent-tabs-mode: nil; tab-width: 4; js2-indent-offset: 4; js2-basic-offset: 4; -*- - * vim: set ts=4 sw=4 et tw=99 ft=js: - */ - -export const ArrayExpression = "ArrayExpression"; -export const ArrayPattern = "ArrayPattern"; -export const ArrowFunctionExpression = "ArrowFunctionExpression"; -export const AssignmentExpression = "AssignmentExpression"; -export const BinaryExpression = "BinaryExpression"; -export const BlockStatement = "BlockStatement"; -export const BreakStatement = "BreakStatement"; -export const CallExpression = "CallExpression"; -export const CatchClause = "CatchClause"; -export const ClassBody = "ClassBody"; -export const ClassDeclaration = "ClassDeclaration"; -export const ClassExpression = "ClassExpression"; -export const ClassHeritage = "ClassHeritage"; -export const ComprehensionBlock = "ComprehensionBlock"; -export const ComprehensionExpression = "ComprehensionExpression"; -export const ConditionalExpression = "ConditionalExpression"; -export const ContinueStatement = "ContinueStatement"; -export const DebuggerStatement = "DebuggerStatement"; -export const DoWhileStatement = "DoWhileStatement"; -export const EmptyStatement = "EmptyStatement"; -export const ExportAllDeclaration = "ExportAllDeclaration"; -export const ExportDefaultDeclaration = "ExportDefaultDeclaration"; -export const ExportNamedDeclaration = "ExportNamedDeclaration"; -export const ExportSpecifier = "ExportSpecifier"; -export const ExpressionStatement = "ExpressionStatement"; -export const ForInStatement = "ForInStatement"; -export const ForOfStatement = "ForOfStatement"; -export const ForStatement = "ForStatement"; -export const FunctionDeclaration = "FunctionDeclaration"; -export const FunctionExpression = "FunctionExpression"; -export const Identifier = "Identifier"; -export const IfStatement = "IfStatement"; -export const ImportDeclaration = "ImportDeclaration"; -export const ImportSpecifier = "ImportSpecifier"; -export const ImportDefaultSpecifier = "ImportDefaultSpecifier"; -export const ImportNamespaceSpecifier = "ImportNamespaceSpecifier"; -export const LabeledStatement = "LabeledStatement"; -export const Literal = "Literal"; -export const LogicalExpression = "LogicalExpression"; -export const MemberExpression = "MemberExpression"; -export const MetaProperty = "MetaProperty"; -export const MethodDefinition = "MethodDefinition"; -export const ModuleDeclaration = "ModuleDeclaration"; -export const NewExpression = "NewExpression"; -export const ObjectExpression = "ObjectExpression"; -export const ObjectPattern = "ObjectPattern"; -export const Program = "Program"; -export const Property = "Property"; -export const RestElement = "RestElement"; -export const ReturnStatement = "ReturnStatement"; -export const SequenceExpression = "SequenceExpression"; -export const SpreadElement = "SpreadElement"; -export const Super = "Super"; -export const SwitchCase = "SwitchCase"; -export const SwitchStatement = "SwitchStatement"; -export const TaggedTemplateExpression = "TaggedTemplateExpression"; -export const TemplateElement = "TemplateElement"; -export const TemplateLiteral = "TemplateLiteral"; -export const ThisExpression = "ThisExpression"; -export const ThrowStatement = "ThrowStatement"; -export const TryStatement = "TryStatement"; -export const UnaryExpression = "UnaryExpression"; -export const UpdateExpression = "UpdateExpression"; -export const VariableDeclaration = "VariableDeclaration"; -export const VariableDeclarator = "VariableDeclarator"; -export const WhileStatement = "WhileStatement"; -export const WithStatement = "WithStatement"; -export const YieldExpression = "YieldExpression"; - -function isNotNull(n) { - if (!n) throw new Error("assertion failed: value is null or undefined"); - return true; -} -function hasType(n) { - if (!n.type) - throw new Error( - `assertion failed: value ${JSON.stringify(n)} does not have a 'type:' property` - ); - return true; -} - -function isast(n) { - return isNotNull(n) && hasType(n) && n; -} - -function isnullableast(n) { - return (!n || hasType(n)) && n; -} - -function isastarray(n) { - if (!Array.isArray(n)) throw new Error("value must be an array"); - for (let el of n) isast(el); - return n; -} - -function isboolean(n) { - if (typeof n !== "boolean") throw new Error("value must be a boolean"); - return n; -} - -// esprima currently fails to parse arrow functions with default argument values, so we make those function expressions - -export function arrayExpression(els = []) { - return { type: ArrayExpression, elements: els }; -} -export function arrowFunctionExpression(params, body, defaults = [], expression = false) { - return { - type: ArrowFunctionExpression, - params: params.map(isast), - defaults: defaults.map(isast), - body: isast(body), - expression: expression, - }; -} - -export function assignmentExpression(l, op, r) { - return { - type: AssignmentExpression, - operator: op, - left: isast(l), - right: isast(r), - }; -} -export function binaryExpression(l, op, r) { - return { - type: BinaryExpression, - operator: op, - left: isast(l), - right: isast(r), - }; -} -export function blockStatement(stmts = [], loc = null) { - return { type: BlockStatement, body: stmts.map(isast), loc: loc }; -} -export function breakStatement(label) { - return { type: BreakStatement, label: isast(label) }; -} -export function callExpression(callee, args = []) { - return { - type: CallExpression, - callee: isast(callee), - arguments: args.map(isast), - }; -} -export function catchClause(param, body, guard) { - return { - type: CatchClause, - body: body, - param: isast(param), - guard: isnullableast(guard), - }; -} -export function conditionalExpression(test, consequent, alternate) { - return { - type: ConditionalExpression, - test: isast(test), - consequent: isast(consequent), - alternate: isast(alternate), - }; -} -export function continueStatement(label) { - return { type: ContinueStatement, label: isnullableast(label) }; -} -export function emptyStatement() { - return { type: EmptyStatement }; -} -export function expressionStatement(exp) { - return { type: ExpressionStatement, expression: isast(exp) }; -} -export function forInStatement(left, right, body) { - return { - type: ForInStatement, - left: isast(left), - right: isast(right), - body: isast(body), - }; -} -export function forOfStatement(left, right, body) { - return { - type: ForOfStatement, - left: isast(left), - right: isast(right), - body: isast(body), - }; -} -export function forStatement(init, test, update, body) { - return { - type: ForStatement, - init: isast(init), - test: isast(test), - update: isast(update), - body: isast(body), - }; -} -export function functionDeclaration(id, params, body, defaults = []) { - return { - type: FunctionDeclaration, - id: isast(id), - params: params.map(isast), - body: isast(body), - defaults: defaults.map(isnullableast), - generator: false, - expression: false, - }; -} -export function functionExpression(id, params, body, defaults = []) { - return { - type: FunctionExpression, - id: isnullableast(id), - params: params.map(isast), - body: isast(body), - defaults: defaults.map(isnullableast), - generator: false, - expression: false, - }; -} -export function identifier(name) { - return { type: Identifier, name: name }; -} -export function ifStatement(test, consequent, alternate) { - return { - type: IfStatement, - test: isast(test), - consequent: isast(consequent), - alternate: isnullableast(alternate), - }; -} -export function labeledStatement(label, body) { - return { - type: LabeledStatement, - label: isast(label), - body: body.map(isast), - }; -} -export function literal(val) { - return { - type: Literal, - value: val, - raw: typeof val === "string" ? `\'${val}\'` : `${val}`, - }; -} -export function logicalExpression(l, op, r) { - return { - type: LogicalExpression, - left: isast(l), - right: isast(r), - operator: op, - }; -} -export function memberExpression(obj, prop, computed = false) { - return { - type: MemberExpression, - object: isast(obj), - property: isast(prop), - computed: isboolean(computed), - }; -} -export function metaProperty(meta, property) { - return { type: MetaProperty, meta, property }; -} -export function methodDefinition(key, value, kind = "init") { - return { type: MethodDefinition, key: key, value: value, kind: kind }; -} -export function objectExpression(properties) { - return { type: ObjectExpression, properties: properties.map(isast) }; -} -export function property(key, value, kind = "init", computed = false) { - return { - type: Property, - key: isast(key), - value: isast(value), - kind: kind, - computed: isboolean(computed), - }; -} -export function restElement(arg) { - return { type: RestElement, argument: isast(arg) }; -} -export function returnStatement(arg) { - return { type: ReturnStatement, argument: isast(arg) }; -} -export function sequenceExpression(expressions) { - return { type: SequenceExpression, expressions: expressions.map(isast) }; -} -export function spreadElement(arg) { - return { type: SpreadElement, argument: arg }; -} -export function superExpression() { - return { type: Super }; -} -export function switchCase(test, consequent) { - return { - type: SwitchCase, - test: isnullableast(test), - consequent: isastarray(consequent), - }; -} -export function thisExpression() { - return { type: ThisExpression }; -} -export function throwStatement(arg) { - return { type: ThrowStatement, argument: isast(arg) }; -} -export function tryStatement(block, handlers, finalizer) { - return { - type: TryStatement, - block: block, - handlers: handlers, - guardedHandlers: [], - finalizer: finalizer, - }; -} -export function unaryExpression(op, arg) { - return { type: UnaryExpression, operator: op, argument: isast(arg) }; -} -export function variableDeclaration(kind, ...rest) { - if (Array.isArray(rest[0])) { - // we assume it's an array of declarators and use it as such - return { - type: VariableDeclaration, - kind: kind, - declarations: rest[0].map(isast), - }; - } else { - // otherwise, we assume it's a list of repeating id+init pairs - if (rest.length % 2 !== 0) - throw new Error( - "variable declarations must have equal numbers of identifiers and initializers" - ); - let decls = []; - while (rest.length > 0) { - decls.push(variableDeclarator(isast(rest.shift()), isnullableast(rest.shift()))); - } - return { type: VariableDeclaration, kind: kind, declarations: decls }; - } -} -export function constDeclaration(...rest) { - return variableDeclaration("const", ...rest); -} -export function letDeclaration(...rest) { - return variableDeclaration("let", ...rest); -} -export function varDeclaration(...rest) { - return variableDeclaration("var", ...rest); -} - -export function variableDeclarator(id, init = undefined) { - return { type: VariableDeclarator, id: isast(id), init: init }; -} -export function whileStatement(test, body) { - return { type: WhileStatement, test: isast(test), body: isast(body) }; -} - -export function undefinedLit() { - return unaryExpression("void", literal(0)); -} -export function nullLit() { - return literal(null); -} diff --git a/lib/ast-builder.ts b/lib/ast-builder.ts new file mode 100644 index 00000000..eff79e1f --- /dev/null +++ b/lib/ast-builder.ts @@ -0,0 +1,404 @@ +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: + */ + +// Typed constructors for the compiler's ESTree dialect (see estree.ts), +// plus the node-type string constants the passes switch over. + +import type * as e from "./estree"; + +export const ArrayExpression = "ArrayExpression" as const; +export const ArrayPattern = "ArrayPattern" as const; +export const AssignmentPattern = "AssignmentPattern" as const; +export const ArrowFunctionExpression = "ArrowFunctionExpression" as const; +export const AssignmentExpression = "AssignmentExpression" as const; +export const BinaryExpression = "BinaryExpression" as const; +export const BlockStatement = "BlockStatement" as const; +export const BreakStatement = "BreakStatement" as const; +export const CallExpression = "CallExpression" as const; +export const CatchClause = "CatchClause" as const; +export const ClassBody = "ClassBody" as const; +export const ClassDeclaration = "ClassDeclaration" as const; +export const ClassExpression = "ClassExpression" as const; +export const ClassHeritage = "ClassHeritage" as const; +export const ComprehensionBlock = "ComprehensionBlock" as const; +export const ComprehensionExpression = "ComprehensionExpression" as const; +export const ConditionalExpression = "ConditionalExpression" as const; +export const ContinueStatement = "ContinueStatement" as const; +export const DebuggerStatement = "DebuggerStatement" as const; +export const DoWhileStatement = "DoWhileStatement" as const; +export const EmptyStatement = "EmptyStatement" as const; +export const ExportAllDeclaration = "ExportAllDeclaration" as const; +export const ExportDefaultDeclaration = "ExportDefaultDeclaration" as const; +export const ExportNamedDeclaration = "ExportNamedDeclaration" as const; +export const ExportSpecifier = "ExportSpecifier" as const; +export const ExpressionStatement = "ExpressionStatement" as const; +export const ForInStatement = "ForInStatement" as const; +export const ForOfStatement = "ForOfStatement" as const; +export const ForStatement = "ForStatement" as const; +export const FunctionDeclaration = "FunctionDeclaration" as const; +export const FunctionExpression = "FunctionExpression" as const; +export const Identifier = "Identifier" as const; +export const IfStatement = "IfStatement" as const; +export const ImportDeclaration = "ImportDeclaration" as const; +export const ImportSpecifier = "ImportSpecifier" as const; +export const ImportDefaultSpecifier = "ImportDefaultSpecifier" as const; +export const ImportNamespaceSpecifier = "ImportNamespaceSpecifier" as const; +export const LabeledStatement = "LabeledStatement" as const; +export const Literal = "Literal" as const; +export const LogicalExpression = "LogicalExpression" as const; +export const MemberExpression = "MemberExpression" as const; +export const MetaProperty = "MetaProperty" as const; +export const MethodDefinition = "MethodDefinition" as const; +export const ModuleDeclaration = "ModuleDeclaration" as const; +export const NewExpression = "NewExpression" as const; +export const ObjectExpression = "ObjectExpression" as const; +export const ObjectPattern = "ObjectPattern" as const; +export const Program = "Program" as const; +export const Property = "Property" as const; +export const RestElement = "RestElement" as const; +export const ReturnStatement = "ReturnStatement" as const; +export const SequenceExpression = "SequenceExpression" as const; +export const SpreadElement = "SpreadElement" as const; +export const Super = "Super" as const; +export const SwitchCase = "SwitchCase" as const; +export const SwitchStatement = "SwitchStatement" as const; +export const TaggedTemplateExpression = "TaggedTemplateExpression" as const; +export const TemplateElement = "TemplateElement" as const; +export const TemplateLiteral = "TemplateLiteral" as const; +export const ThisExpression = "ThisExpression" as const; +export const ThrowStatement = "ThrowStatement" as const; +export const TryStatement = "TryStatement" as const; +export const UnaryExpression = "UnaryExpression" as const; +export const UpdateExpression = "UpdateExpression" as const; +export const VariableDeclaration = "VariableDeclaration" as const; +export const VariableDeclarator = "VariableDeclarator" as const; +export const WhileStatement = "WhileStatement" as const; +export const WithStatement = "WithStatement" as const; +export const YieldExpression = "YieldExpression" as const; + +export function arrayExpression( + els: (e.Expression | e.SpreadElement | null)[] = [] +): e.ArrayExpression { + return { type: ArrayExpression, elements: els }; +} + +export function arrowFunctionExpression( + params: e.Pattern[], + body: e.BlockStatement | e.Expression, + defaults: (e.Expression | null)[] = [], + expression = false +): e.ArrowFunctionExpression { + return { + type: ArrowFunctionExpression, + id: null, + params, + defaults, + body, + generator: false, + expression, + }; +} + +export function assignmentExpression( + l: e.Expression | e.Pattern, + op: e.AssignmentOperator, + r: e.Expression +): e.AssignmentExpression { + return { type: AssignmentExpression, operator: op, left: l, right: r }; +} + +export function binaryExpression( + l: e.Expression, + op: e.BinaryOperator, + r: e.Expression +): e.BinaryExpression { + return { type: BinaryExpression, operator: op, left: l, right: r }; +} + +export function blockStatement( + stmts: e.Statement[] = [], + loc: e.SourceLocation | null = null +): e.BlockStatement { + return { type: BlockStatement, body: stmts, loc }; +} + +export function breakStatement(label: e.Identifier | null = null): e.BreakStatement { + return { type: BreakStatement, label }; +} + +export function callExpression( + callee: e.Expression | e.Super, + args: (e.Expression | e.SpreadElement)[] = [] +): e.CallExpression { + return { type: CallExpression, callee, arguments: args }; +} + +export function catchClause( + param: e.Pattern, + body: e.BlockStatement, + guard: e.Expression | null = null +): e.CatchClause { + return { type: CatchClause, body, param, guard }; +} + +export function conditionalExpression( + test: e.Expression, + consequent: e.Expression, + alternate: e.Expression +): e.ConditionalExpression { + return { type: ConditionalExpression, test, consequent, alternate }; +} + +export function continueStatement(label: e.Identifier | null = null): e.ContinueStatement { + return { type: ContinueStatement, label }; +} + +export function emptyStatement(): e.EmptyStatement { + return { type: EmptyStatement }; +} + +export function expressionStatement(exp: e.Expression): e.ExpressionStatement { + return { type: ExpressionStatement, expression: exp }; +} + +export function forInStatement( + left: e.VariableDeclaration | e.Pattern, + right: e.Expression, + body: e.Statement +): e.ForInStatement { + return { type: ForInStatement, left, right, body }; +} + +export function forOfStatement( + left: e.VariableDeclaration | e.Pattern, + right: e.Expression, + body: e.Statement +): e.ForOfStatement { + return { type: ForOfStatement, left, right, body }; +} + +export function forStatement( + init: e.VariableDeclaration | e.Expression | null, + test: e.Expression | null, + update: e.Expression | null, + body: e.Statement +): e.ForStatement { + return { type: ForStatement, init, test, update, body }; +} + +export function functionDeclaration( + id: e.Identifier, + params: e.Pattern[], + body: e.BlockStatement, + defaults: (e.Expression | null)[] = [] +): e.FunctionDeclaration { + return { + type: FunctionDeclaration, + id, + params, + body, + defaults, + generator: false, + expression: false, + }; +} + +export function functionExpression( + id: e.Identifier | null, + params: e.Pattern[], + body: e.BlockStatement, + defaults: (e.Expression | null)[] = [] +): e.FunctionExpression { + return { + type: FunctionExpression, + id, + params, + body, + defaults, + generator: false, + expression: false, + }; +} + +export function identifier(name: string): e.Identifier { + return { type: Identifier, name }; +} + +export function ifStatement( + test: e.Expression, + consequent: e.Statement, + alternate: e.Statement | null = null +): e.IfStatement { + return { type: IfStatement, test, consequent, alternate }; +} + +export function labeledStatement(label: e.Identifier, body: e.Statement): e.LabeledStatement { + return { type: LabeledStatement, label, body }; +} + +export function literal(val: string | number | boolean | null): e.Literal { + return { + type: Literal, + value: val, + raw: typeof val === "string" ? `'${val}'` : `${val}`, + }; +} + +export function logicalExpression( + l: e.Expression, + op: "||" | "&&", + r: e.Expression +): e.LogicalExpression { + return { type: LogicalExpression, left: l, right: r, operator: op }; +} + +export function memberExpression( + obj: e.Expression | e.Super, + prop: e.Expression, + computed = false +): e.MemberExpression { + return { type: MemberExpression, object: obj, property: prop, computed }; +} + +export function metaProperty(meta: string, property: string): e.MetaProperty { + return { type: MetaProperty, meta, property }; +} + +export function methodDefinition( + key: e.Expression, + value: e.FunctionExpression, + kind: e.MethodDefinition["kind"] = "init" +): e.MethodDefinition { + return { type: MethodDefinition, key, value, kind }; +} + +export function objectExpression(properties: e.Property[]): e.ObjectExpression { + return { type: ObjectExpression, properties }; +} + +export function property( + key: e.Expression, + value: e.Expression | e.Pattern, + kind: e.Property["kind"] = "init", + computed = false +): e.Property { + return { type: Property, key, value, kind, computed }; +} + +export function restElement(arg: e.Pattern): e.RestElement { + return { type: RestElement, argument: arg }; +} + +export function returnStatement(arg: e.Expression | null): e.ReturnStatement { + return { type: ReturnStatement, argument: arg }; +} + +export function sequenceExpression(expressions: e.Expression[]): e.SequenceExpression { + return { type: SequenceExpression, expressions }; +} + +export function spreadElement(arg: e.Expression): e.SpreadElement { + return { type: SpreadElement, argument: arg }; +} + +export function superExpression(): e.Super { + return { type: Super }; +} + +export function switchCase(test: e.Expression | null, consequent: e.Statement[]): e.SwitchCase { + return { type: SwitchCase, test, consequent }; +} + +export function thisExpression(): e.ThisExpression { + return { type: ThisExpression }; +} + +export function throwStatement(arg: e.Expression): e.ThrowStatement { + return { type: ThrowStatement, argument: arg }; +} + +export function tryStatement( + block: e.BlockStatement, + handlers: e.CatchClause[], + finalizer: e.BlockStatement | null = null +): e.TryStatement { + return { type: TryStatement, block, handlers, guardedHandlers: [], finalizer }; +} + +export function unaryExpression( + op: e.UnaryExpression["operator"], + arg: e.Expression +): e.UnaryExpression { + return { type: UnaryExpression, operator: op, argument: arg }; +} + +type DeclPair = [e.Pattern, e.Expression | null]; + +// two call shapes: an array of declarators, or alternating id+init +// arguments (id1, init1, id2, init2, ...) +export function variableDeclaration( + kind: e.VariableDeclaration["kind"], + declarations: e.VariableDeclarator[] +): e.VariableDeclaration; +export function variableDeclaration( + kind: e.VariableDeclaration["kind"], + ...pairs: (e.Pattern | e.Expression | null)[] +): e.VariableDeclaration; +export function variableDeclaration( + kind: e.VariableDeclaration["kind"], + ...rest: (e.VariableDeclarator[] | e.Pattern | e.Expression | null)[] +): e.VariableDeclaration { + const first = rest[0]; + if (Array.isArray(first)) { + return { type: VariableDeclaration, kind, declarations: first }; + } + if (rest.length % 2 !== 0) + throw new Error( + "variable declarations must have equal numbers of identifiers and initializers" + ); + const decls: e.VariableDeclarator[] = []; + for (let i = 0; i < rest.length; i += 2) { + const id = rest[i] as e.Pattern; + const init = (rest[i + 1] as e.Expression | null) ?? null; + decls.push(variableDeclarator(id, init)); + } + return { type: VariableDeclaration, kind, declarations: decls }; +} + +export function constDeclaration( + ...rest: (e.Pattern | e.Expression | null)[] +): e.VariableDeclaration { + return variableDeclaration("const", ...rest); +} + +export function letDeclaration( + ...rest: (e.Pattern | e.Expression | null)[] +): e.VariableDeclaration { + return variableDeclaration("let", ...rest); +} + +export function varDeclaration( + ...rest: (e.Pattern | e.Expression | null)[] +): e.VariableDeclaration { + return variableDeclaration("var", ...rest); +} + +export function variableDeclarator( + id: e.Pattern, + init: e.Expression | null | undefined = undefined +): e.VariableDeclarator { + return { type: VariableDeclarator, id, init }; +} + +export function whileStatement(test: e.Expression, body: e.Statement): e.WhileStatement { + return { type: WhileStatement, test, body }; +} + +export function undefinedLit(): e.UnaryExpression { + return unaryExpression("void", literal(0)); +} + +export function nullLit(): e.Literal { + return literal(null); +} diff --git a/lib/buck-gen-js.sh b/lib/buck-gen-js.sh new file mode 100644 index 00000000..9ec0ad45 --- /dev/null +++ b/lib/buck-gen-js.sh @@ -0,0 +1,71 @@ +#!/bin/bash +# Invoked by //lib:generated. Produces the equivalent of lib/generated/: +# the compiler (the //lib:tsjs tree — ES-module JS) converted to +# CommonJS so stage0 can run under node, with the same import rewrites +# lib/Makefile applied: +# "@llvm" -> "llvm" (resolved via NODE_PATH to node-llvm) +# "@node-compat/"-> "" (use node's own os/path/fs/...) +# +# The module conversion is tsc in --allowJs transpile mode (compiler-P2; +# this step was babel until then). typescript comes from the repo's +# node_modules, which buck2 doesn't track as an input (same treatment as +# in buck-gen-tsjs.sh). The repo root is recovered from $TMP, which +# buck2 always places under /buck-out/. +set -euo pipefail + +TSJS="$1" # //lib:tsjs — $TSJS/ejs-es6.js + $TSJS/lib/**.js + +REPO="${TMP%%/buck-out/*}" +TSC="$REPO/node_modules/typescript/bin/tsc" + +mkdir -p "$OUT" +OUTABS="$(cd "$OUT" && pwd)" + +# stage the ES-module tree, applying the import rewrites on the way in +# (pre-conversion: tsc then turns the rewritten imports into require()s) +STAGE="$TMP/genjs-stage" +rm -rf "$STAGE" +mkdir -p "$STAGE" + +stage_one() { + local src="$1" dst="$STAGE/$2" + mkdir -p "$(dirname "$dst")" + sed -e 's,"@llvm","llvm",' -e "s,'@llvm','llvm'," -e 's,@node-compat/,,' \ + "$src" > "$dst" +} + +(cd "$TSJS/lib" && find . -name "*.js" | sed 's,^\./,,') | while read -r f; do + stage_one "$TSJS/lib/$f" "lib/$f" +done + +stage_one "$TSJS/ejs-es6.js" "ejs-es6.js" + +cd "$SRCDIR" + +# host-config.js is generated (staged at $SRCDIR root by the genrule) +stage_one host-config.js "lib/host-config.js" + +for f in esprima/esprima-es6.js \ + escodegen/escodegen-es6.js \ + estraverse/estraverse-es6.js \ + esutils/esutils-es6.js \ + esutils/lib/code.js \ + esutils/lib/keyword.js \ + esutils/lib/ast.js; do + stage_one "compiler-js/$f" "external-deps/$f" +done + +# one tsc transpile over the whole tree: ES modules -> CommonJS. +# --allowJs only, no checkJs — no type-checking, just the module +# conversion babel used to do. --esModuleInterop matches babel's +# default/namespace-import interop against CJS modules (llvm, glob, ...). +JS_FILES=$(cd "$STAGE" && find . -name "*.js" | sort) +(cd "$STAGE" && node "$TSC" \ + --ignoreConfig \ + --allowJs \ + --target es2016 \ + --module commonjs \ + --esModuleInterop \ + --rootDir . \ + --outDir "$OUTABS" \ + $JS_FILES) diff --git a/lib/buck-gen-tsjs.sh b/lib/buck-gen-tsjs.sh new file mode 100644 index 00000000..4050223c --- /dev/null +++ b/lib/buck-gen-tsjs.sh @@ -0,0 +1,74 @@ +#!/bin/bash +# Invoked by //lib:tsjs. Produces the compiler as plain ES-module JS: +# .ts sources compile through tsc (strict; flags mirror tsconfig.json), +# .js sources copy through unchanged (the port is incremental). Output +# layout: +# $OUT/ejs-es6.js +# $OUT/lib/{*.js, passes/*.js, eir/*.js} +# Both //lib:generated (the CommonJS conversion for the node-hosted +# stage0) and //:srcdir-tree (stage1+ self-compiles) consume this tree. +# +# typescript comes from the repo's node_modules, which buck2 doesn't +# track as an input (same treatment as in buck-gen-js.sh). +set -euo pipefail + +REPO="${TMP%%/buck-out/*}" +TSC="$REPO/node_modules/typescript/bin/tsc" + +mkdir -p "$OUT" +OUTABS="$(cd "$OUT" && pwd)" + +cd "$SRCDIR" + +# stage into the output layout; tsc emits over the same tree +STAGE="$TMP/tsjs-stage" +rm -rf "$STAGE" +mkdir -p "$STAGE/lib" + +for f in *.js *.ts passes/*.js passes/*.ts eir/*.js eir/*.ts; do + [ -e "$f" ] || continue + case "$f" in + ejs-es6.js|ejs-es6.ts) continue ;; + esac + mkdir -p "$STAGE/lib/$(dirname "$f")" + cp "$f" "$STAGE/lib/$f" +done +for f in ejs-es6.js ejs-es6.ts; do + if [ -e "$f" ]; then cp "$f" "$STAGE/$f"; fi +done + +# hand-written surface declarations for the vendored external-deps JS, +# committed in the esprima/escodegen submodules next to their .js +# (relative imports like ../../external-deps/escodegen/escodegen-es6 +# typecheck against these; the .js resolves at runtime) +if [ -d compiler-js ]; then + (cd compiler-js && find . -name "*.d.ts" | while read -r f; do + mkdir -p "$STAGE/external-deps/$(dirname "$f")" + cp "$f" "$STAGE/external-deps/$f" + done) +fi + +# copy the .js files through +(cd "$STAGE" && find . -name "*.js" | while read -r f; do + mkdir -p "$OUTABS/$(dirname "$f")" + cp "$f" "$OUTABS/$f" +done) + +# compile the .ts files (flags mirror tsconfig.json) +TS_FILES=$(cd "$STAGE" && find . -name "*.ts" | sort) +if [ -n "$TS_FILES" ]; then + (cd "$STAGE" && node "$TSC" \ + --ignoreConfig \ + --strict \ + --noUncheckedIndexedAccess \ + --noImplicitOverride \ + --noEmitOnError \ + --target es2016 \ + --module esnext \ + --moduleResolution bundler \ + --types node \ + --typeRoots "$REPO/node_modules/@types" \ + --rootDir . \ + --outDir "$OUTABS" \ + $TS_FILES) +fi diff --git a/lib/closure-conversion.js b/lib/closure-conversion.js deleted file mode 100644 index 663cf611..00000000 --- a/lib/closure-conversion.js +++ /dev/null @@ -1,90 +0,0 @@ -/* -*- Mode: js2; indent-tabs-mode: nil; tab-width: 4; js2-indent-offset: 4; js2-basic-offset: 4; -*- - * vim: set ts=4 sw=4 et tw=99 ft=js: - */ - -import { DesugarArguments } from "./passes/desugar-arguments"; -import { DesugarImportExport } from "./passes/desugar-import-export"; -import { DesugarClasses } from "./passes/desugar-classes"; -import { DesugarDestructuring } from "./passes/desugar-destructuring"; -import { DesugarUpdateAssignments } from "./passes/desugar-update-assignments"; -import { DesugarTemplates } from "./passes/desugar-templates"; -import { DesugarArrowFunctions } from "./passes/desugar-arrow-functions"; -import { DesugarGeneratorFunctions } from "./passes/desugar-generator-functions"; -import { DesugarDefaults } from "./passes/desugar-defaults"; -import { DesugarRestParameters } from "./passes/desugar-rest-parameters"; -import { DesugarForOf } from "./passes/desugar-for-of"; -import { DesugarSpread } from "./passes/desugar-spread"; -import { DesugarMetaProperties } from "./passes/desugar-metaproperties"; -import { HoistFuncDecls } from "./passes/hoist-func-decls"; -import { FuncDeclsToVars } from "./passes/func-decls-to-vars"; -import { DesugarLetLoopVars } from "./passes/desugar-let-loopvars"; -import { HoistVars } from "./passes/hoist-vars"; -import { NameAnonymousFunctions } from "./passes/name-anonymous-functions"; -import { NewClosureConvert } from "./passes/new-cc"; -//import { IIFEIdioms } from './passes/iife-idioms'; -import { LambdaLift } from "./passes/lambda-lift"; - -import * as escodegen from "../external-deps/escodegen/escodegen-es6"; -import * as debug from "./debug"; - -// the HoistFuncDecls phase transforms the AST to give v8 semantics -// when faced with multiple function declarations within the same -// function scope. -// -const enable_hoist_func_decls_pass = true; - -const passes = [ - DesugarImportExport, - DesugarClasses, - DesugarRestParameters, - DesugarDestructuring, - DesugarUpdateAssignments, - DesugarTemplates, - DesugarGeneratorFunctions, - DesugarArrowFunctions, - DesugarDefaults, - DesugarForOf, - DesugarSpread, - DesugarMetaProperties, - enable_hoist_func_decls_pass ? HoistFuncDecls : null, - FuncDeclsToVars, - DesugarLetLoopVars, - HoistVars, - NameAnonymousFunctions, - DesugarArguments, - NewClosureConvert, - //IIFEIdioms, - LambdaLift, -]; - -export function convert(tree, filename, modules, options) { - debug.log("before:"); - debug.log(() => escodegen.generate(tree)); - - passes.forEach((passType) => { - if (!passType) return; - try { - debug.time(2, passType.name); - let pass = new passType(options, filename, modules); - tree = pass.visit(tree); - debug.timeEnd(2, passType.name); - if (options.debug_passes.has(passType.name)) { - console.log(`after: ${passType.name}`); - console.log(escodegen.generate(tree)); - } - - debug.log(2, `after: ${passType.name}`); - debug.log(2, () => escodegen.generate(tree)); - debug.log(3, () => { - if (typeof __ejs != "undefined") - __ejs.GC.dumpAllocationStats(`after ${passType.name}`); - }); - } catch (e) { - debug.log(2, `exception in pass ${passType.name}`); - debug.log(2, e); - throw e; - } - }); - - return tree; -} diff --git a/lib/common-ids.js b/lib/common-ids.ts similarity index 92% rename from lib/common-ids.js rename to lib/common-ids.ts index 9a5d37ae..c20281c2 100644 --- a/lib/common-ids.js +++ b/lib/common-ids.ts @@ -1,5 +1,5 @@ -/* -*- Mode: js2; indent-tabs-mode: nil; tab-width: 4; js2-indent-offset: 4; js2-basic-offset: 4; -*- - * vim: set ts=4 sw=4 et tw=99 ft=js: +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: */ import { identifier } from "./ast-builder"; @@ -11,12 +11,15 @@ export const makeClosureNoEnv_id = identifier("%makeClosureNoEnv"); export const makeAnonClosure_id = identifier("%makeAnonClosure"); export const makeGenerator_id = identifier("%makeGenerator"); export const generatorYield_id = identifier("%generatorYield"); +export const generatorIsReturnSentinel_id = identifier("%generatorIsReturnSentinel"); +export const generatorReturnValue_id = identifier("%generatorReturnValue"); export const setSlot_id = identifier("%setSlot"); export const slot_id = identifier("%slot"); export const invokeClosure_id = identifier("%invokeClosure"); export const constructClosure_id = identifier("%constructClosure"); export const constructSuper_id = identifier("%constructSuper"); export const constructSuperApply_id = identifier("%constructSuperApply"); +export const constructApply_id = identifier("%constructApply"); export const setLocal_id = identifier("%setLocal"); export const setGlobal_id = identifier("%setGlobal"); export const getLocal_id = identifier("%getLocal"); diff --git a/lib/compiler.js b/lib/compiler.js deleted file mode 100644 index 8234203b..00000000 --- a/lib/compiler.js +++ /dev/null @@ -1,3559 +0,0 @@ -/* -*- Mode: js2; indent-tabs-mode: nil; tab-width: 4; js2-indent-offset: 4; js2-basic-offset: 4; -*- - * vim: set ts=4 sw=4 et tw=99 ft=js: - */ - -import * as llvm from "@llvm"; - -import { Stack } from "./stack-es6"; -import { TreeVisitor } from "./node-visitor"; - -import { generate as escodegenerate } from "../external-deps/escodegen/escodegen-es6"; -import { convert as closure_convert } from "./closure-conversion"; -import * as optimizations from "./optimizations"; -import * as types from "./types"; -import * as consts from "./consts"; -import * as runtime from "./runtime"; -import * as debug from "./debug"; - -import * as b from "./ast-builder"; -import { startGenerator, is_intrinsic } from "./echo-util"; -import { - ExitableScope, - TryExitableScope, - SwitchExitableScope, - LoopExitableScope, - LabeledStatementExitableScope, -} from "./exitable-scope"; - -import { ABI } from "./abi"; -import { SRetABI } from "./sret-abi"; - -let ir = llvm.IRBuilder; - -let hasOwn = Object.prototype.hasOwnProperty; - -class LLVMIRVisitor extends TreeVisitor { - constructor(module, filename, triple, options, abi, allModules, this_module_info, dibuilder, difile) { - super(); - this.module = module; - this.filename = filename; - this.triple = triple; - this.options = options; - this.abi = abi; - this.allModules = allModules; - this.this_module_info = this_module_info; - this.dibuilder = dibuilder; - this.difile = difile; - - this.idgen = startGenerator(); - - if (this.options.record_types) this.genRecordId = startGenerator(); - - // build up our runtime method table - this.ejs_intrinsics = Object.create(null, { - templateDefaultHandlerCall: { - value: this.handleTemplateDefaultHandlerCall, - }, - templateCallsite: { value: this.handleTemplateCallsite }, - moduleGet: { value: this.handleModuleGet }, - moduleGetSlot: { value: this.handleModuleGetSlot }, - moduleSetSlot: { value: this.handleModuleSetSlot }, - moduleGetExotic: { value: this.handleModuleGetExotic }, - getArgumentsObject: { value: this.handleGetArgumentsObject }, - getLocal: { value: this.handleGetLocal }, - setLocal: { value: this.handleSetLocal }, - getGlobal: { value: this.handleGetGlobal }, - setGlobal: { value: this.handleSetGlobal }, - getArg: { value: this.handleGetArg }, - getNewTarget: { value: this.handleGetNewTarget }, - slot: { value: this.handleGetSlot }, - setSlot: { value: this.handleSetSlot }, - invokeClosure: { value: this.handleInvokeClosure }, - constructClosure: { value: this.handleConstructClosure }, - constructSuper: { value: this.handleConstructSuper }, - constructSuperApply: { value: this.handleConstructSuperApply }, - setConstructorKindDerived: { - value: this.handleSetConstructorKindDerived, - }, - setConstructorKindBase: { - value: this.handleSetConstructorKindBase, - }, - makeClosure: { value: this.handleMakeClosure }, - makeClosureNoEnv: { value: this.handleMakeClosureNoEnv }, - makeAnonClosure: { value: this.handleMakeAnonClosure }, - makeGenerator: { value: this.handleMakeGenerator }, - generatorYield: { value: this.handleGeneratorYield }, - createArgScratchArea: { value: this.handleCreateArgScratchArea }, - makeClosureEnv: { value: this.handleMakeClosureEnv }, - typeofIsObject: { value: this.handleTypeofIsObject }, - typeofIsFunction: { value: this.handleTypeofIsFunction }, - typeofIsString: { value: this.handleTypeofIsString }, - typeofIsSymbol: { value: this.handleTypeofIsSymbol }, - typeofIsNumber: { value: this.handleTypeofIsNumber }, - typeofIsBoolean: { value: this.handleTypeofIsBoolean }, - builtinUndefined: { value: this.handleBuiltinUndefined }, - isNullOrUndefined: { value: this.handleIsNullOrUndefined }, - isUndefined: { value: this.handleIsUndefined }, - isNull: { value: this.handleIsNull }, - setPrototypeOf: { value: this.handleSetPrototypeOf }, - objectCreate: { value: this.handleObjectCreate }, - arrayFromRest: { value: this.handleArrayFromRest }, - arrayFromSpread: { value: this.handleArrayFromSpread }, - createIterResult: { value: this.handleCreateIterResult }, - createIteratorWrapper: { value: this.handleCreateIteratorWrapper }, - }); - - this.opencode_intrinsics = { - unaryNot: true, - - templateDefaultHandlerCall: true, - - moduleGet: true, // unused - moduleGetSlot: true, - moduleSetSlot: true, - moduleGetExotic: true, - - getLocal: true, // unused - setLocal: true, // unused - getGlobal: true, // unused - setGlobal: true, // unused - slot: true, - setSlot: true, - - invokeClosure: false, - constructClosure: false, - makeClosure: true, - makeAnonClosure: true, - createArgScratchArea: true, - makeClosureEnv: true, - setConstructorKindDerived: false, - setConstructorKindBase: false, - - typeofIsObject: true, - typeofIsFunction: true, - typeofIsString: true, - typeofIsSymbol: true, - typeofIsNumber: true, - typeofIsBoolean: true, - builtinUndefined: true, - isNullOrUndefined: false, // unused - isUndefined: true, - isNull: true, - }; - - this.llvm_intrinsics = { - gcroot: () => module.getOrInsertIntrinsic("@llvm.gcroot"), - }; - - this.ejs_runtime = runtime.createInterface(module, this.abi); - this.ejs_binops = runtime.createBinopsInterface(module, this.abi); - this.ejs_atoms = runtime.createAtomsInterface(module); - this.ejs_globals = runtime.createGlobalsInterface(module); - this.ejs_symbols = runtime.createSymbolsInterface(module); - - this.module_atoms = new Map(); - - let init_function_name = `_ejs_module_init_string_literals_${this.filename}`; - this.literalInitializationFunction = this.module.getOrInsertFunction( - init_function_name, - types.Void, - [] - ); - - if (this.options.debug) - this.literalInitializationDebugInfo = this.dibuilder.createFunction( - this.difile, - init_function_name, - init_function_name, - this.difile, - 0, - false, - true, - 0, - 0, - true, - this.literalInitializationFunction - ); - - // this function is only ever called by this module's toplevel - this.literalInitializationFunction.setInternalLinkage(); - - // initialize the scope stack with the global (empty) scope - this.scope_stack = new Stack(new Map()); - - let entry_bb = new llvm.BasicBlock("entry", this.literalInitializationFunction); - let return_bb = new llvm.BasicBlock("return", this.literalInitializationFunction); - - if (this.options.debug) - ir.setCurrentDebugLocation( - llvm.DebugLoc.get(0, 0, this.literalInitializationDebugInfo) - ); - - this.doInsideBBlock(entry_bb, () => { - ir.createBr(return_bb); - }); - this.doInsideBBlock(return_bb, () => { - //this.createCall this.ejs_runtime.log, [consts.string(ir, 'done with literal initialization')], '' - ir.createRetVoid(); - }); - - this.literalInitializationBB = entry_bb; - } - - // lots of helper methods - - emitModuleInfo() { - this.this_module_type = types.getModuleSpecificType( - this.this_module_info.module_name, - this.this_module_info.slot_num - ); - - this.this_module_global = new llvm.GlobalVariable( - this.module, - this.this_module_type, - this.this_module_info.module_name, - llvm.Constant.getAggregateZero(this.this_module_type), - true - ); - this.import_module_globals = new Map(); - for (let import_module_string of this.this_module_info.importList) { - let import_module_info = this.allModules.get(import_module_string); - if (!import_module_info.isNative()) - this.import_module_globals.set( - import_module_string, - new llvm.GlobalVariable( - this.module, - types.EjsModule, - import_module_info.module_name, - null, - true - ) - ); - } - this.this_module_initted = new llvm.GlobalVariable( - this.module, - types.Bool, - `${this.this_module_info.module_name}_initialized`, - consts.False(), - false - ); - } - - emitModuleResolution(module_accessors) { - // this.loadUndefinedEjsValue depends on this - this.currentFunction = this.toplevel_function; - - ir.setInsertPoint(this.resolve_modules_bb); - if (this.options.debug) - ir.setCurrentDebugLocation(llvm.DebugLoc.get(0, 0, this.currentFunction.debug_info)); - - let uninitialized_bb = new llvm.BasicBlock("module_uninitialized", this.toplevel_function); - let initialized_bb = new llvm.BasicBlock("module_initialized", this.toplevel_function); - - let load_init_flag = ir.createLoad(types.Bool, this.this_module_initted, "load_init_flag"); - let load_init_cmp = ir.createICmpEq(load_init_flag, consts.False(), "load_init_cmp"); - - ir.createCondBr(load_init_cmp, uninitialized_bb, initialized_bb); - - ir.setInsertPoint(uninitialized_bb); - ir.createStore(consts.True(), this.this_module_initted); - - ir.createCall( - this.literalInitializationFunction.type, - this.literalInitializationFunction, - [], - "" - ); - - // fill in the information we know about this module - // our name - let name_slot = ir.createInBoundsGetElementPointer( - this.this_module_type, - this.this_module_global, - [consts.int32(0), consts.int32(1)], - "name_slot" - ); - ir.createStore(consts.string(ir, this.this_module_info.path), name_slot); - - // num_exports - let num_exports_slot = ir.createInBoundsGetElementPointer( - this.this_module_type, - this.this_module_global, - [consts.int32(0), consts.int32(2)], - "num_exports_slot" - ); - ir.createStore(consts.int32(this.this_module_info.slot_num), num_exports_slot); - - // define our accessor properties - for (let accessor of module_accessors) { - let get_func = - (accessor.getter && accessor.getter.ir_func) || consts.Null(types.EjsClosureFunc); - let set_func = - (accessor.setter && accessor.setter.ir_func) || consts.Null(types.EjsClosureFunc); - let module_arg = ir.createPointerCast( - this.this_module_global, - types.EjsModule.pointerTo(), - "" - ); - ir.createCall( - this.ejs_runtime.module_add_export_accessors.type, - this.ejs_runtime.module_add_export_accessors, - [module_arg, consts.string(ir, accessor.key), get_func, set_func], - "" - ); - } - - for (let import_module_string of this.this_module_info.importList) { - let import_module = this.import_module_globals.get(import_module_string); - if (import_module) { - this.createCall( - this.ejs_runtime.module_resolve, - [import_module], - "", - !this.ejs_runtime.module_resolve.doesNotThrow - ); - } - } - - ir.createBr(this.toplevel_body_bb); - - ir.setInsertPoint(initialized_bb); - return this.createRet(this.loadUndefinedEjsValue()); - } - - // result should be the landingpad's value - beginCatch(result) { - return this.createCall( - this.ejs_runtime.begin_catch, - [ir.createPointerCast(result, types.Int8Pointer, "")], - "begincatch" - ); - } - endCatch() { - return this.createCall(this.ejs_runtime.end_catch, [], "endcatch"); - } - - doInsideExitableScope(scope, f) { - scope.enter(); - f(); - scope.leave(); - } - - doInsideBBlock(b, f) { - let saved = ir.getInsertBlock(); - ir.setInsertPoint(b); - f(); - ir.setInsertPoint(saved); - return b; - } - - createLoad(ty, value, name) { - let rv = ir.createLoad(ty, value, name); - return rv; - } - - createEjsValueLoad(value, name) { - let rv = ir.createLoad(types.EjsValue, value, name); - rv.setAlignment(8); - return rv; - } - - loadCachedEjsValue(name, init) { - let alloca_name = `${name}_alloca`; - let load_name = `${name}_load`; - - let alloca; - if (this.currentFunction[alloca_name]) { - alloca = this.currentFunction[alloca_name]; - } else { - alloca = this.createAlloca(this.currentFunction, types.EjsValue, alloca_name); - this.currentFunction[alloca_name] = alloca; - this.doInsideBBlock(this.currentFunction.entry_bb, () => init(alloca)); - } - - return ir.createLoad(types.EjsValue, alloca, load_name); - } - - loadBoolEjsValue(n) { - let rv = this.loadCachedEjsValue(n, (alloca) => { - let alloca_as_int64 = ir.createBitCast( - alloca, - types.Int64.pointerTo(), - "alloca_as_pointer" - ); - if (n) - ir.createStore( - consts.ejsval_true(this.triple.pointerSize() == 32), - alloca_as_int64 - ); - else - ir.createStore( - consts.ejsval_false(this.triple.pointerSize() == 32), - alloca_as_int64 - ); - }); - rv._ejs_returns_ejsval_bool = true; - return rv; - } - - loadDoubleEjsValue(n) { - return this.loadCachedEjsValue(`num_${n}`, (alloca) => this.storeDouble(alloca, n)); - } - loadNullEjsValue() { - return this.loadCachedEjsValue("null", (alloca) => this.storeNull(alloca)); - } - loadUndefinedEjsValue() { - return this.loadCachedEjsValue("undef", (alloca) => this.storeUndefined(alloca)); - } - - storeUndefined(alloca, name) { - let alloca_as_int64 = ir.createBitCast( - alloca, - types.Int64.pointerTo(), - "alloca_as_pointer" - ); - if (this.triple.pointerSize() === 64) - return ir.createStore( - consts.int64_lowhi(0xfff90000, 0x00000000), - alloca_as_int64, - name - ); - // 32 bit - else - return ir.createStore( - consts.int64_lowhi(0xffffff82, 0x00000000), - alloca_as_int64, - name - ); - } - - storeNull(alloca, name) { - let alloca_as_int64 = ir.createBitCast( - alloca, - types.Int64.pointerTo(), - "alloca_as_pointer" - ); - if (this.triple.pointerSize() === 64) - return ir.createStore( - consts.int64_lowhi(0xfffb8000, 0x00000000), - alloca_as_int64, - name - ); - // 32 bit - else - return ir.createStore( - consts.int64_lowhi(0xffffff87, 0x00000000), - alloca_as_int64, - name - ); - } - - storeDouble(alloca, jsnum, name) { - let c = llvm.ConstantFP.getDouble(jsnum); - let alloca_as_double = ir.createBitCast( - alloca, - types.Double.pointerTo(), - "alloca_as_pointer" - ); - return ir.createStore(c, alloca_as_double, name); - } - - storeBoolean(alloca, jsbool, name) { - let alloca_as_int64 = ir.createBitCast( - alloca, - types.Int64.pointerTo(), - "alloca_as_pointer" - ); - if (this.triple.pointerSize() === 64) - return ir.createStore( - consts.int64_lowhi(0xfff98000, jsbool ? 0x00000001 : 0x000000000), - alloca_as_int64, - name - ); - else - return ir.createStore( - consts.int64_lowhi(0xffffff83, jsbool ? 0x00000001 : 0x000000000), - alloca_as_int64, - name - ); - } - - storeToDest(dest, arg, name = "") { - if (!arg) arg = { type: b.Literal, value: null }; - - if (arg.type === b.Literal) { - if (arg.value === null) return this.storeNull(dest, name); - - if (arg.value === undefined) return this.storeUndefined(dest, name); - - if (typeof arg.value === "number") return this.storeDouble(dest, arg.value, name); - - if (typeof arg.value === "boolean") return this.storeBoolean(dest, arg.value, name); - - // if typeof arg is 'string' - let val = this.visit(arg); - return ir.createStore(val, dest, name); - } else { - let val = this.visit(arg); - return ir.createStore(val, dest, name); - } - } - - storeGlobal(prop, value) { - let gname; - // we store obj.prop, prop is an id - if (prop.type === b.Identifier) gname = prop.name; // prop.type is b.Literal - else gname = prop.value; - - let c = this.getAtom(gname); - - debug.log(() => `createPropertyStore %global[${gname}]`); - - return this.createCall( - this.ejs_runtime.global_setprop, - [c, value], - `globalpropstore_${gname}` - ); - } - - loadGlobal(prop) { - let gname = prop.name; - - if (this.options.frozen_global) - return ir.createLoad(types.EjsValue, this.ejs_globals[prop.name], `load-${gname}`); - - let pname = this.getAtom(gname); - return this.createCall(this.ejs_runtime.global_getprop, [pname], `globalloadprop_${gname}`); - } - - visitWithScope(scope, children) { - this.scope_stack.push(scope); - for (let child of children) this.visit(child); - this.scope_stack.pop(); - } - - findIdentifierInScope(ident) { - for (let scope of this.scope_stack.stack) { - if (scope.has(ident)) return scope.get(ident); - } - return null; - } - - createAlloca(func, type, name) { - let saved_insert_point = ir.getInsertBlock(); - ir.setInsertPointStartBB(func.entry_bb); - let alloca = ir.createAlloca(type, name); - - // if EjsValue was a pointer value we would be able to use an the llvm gcroot intrinsic here. but with the nan boxing - // we kinda lose out as the llvm IR code doesn't permit non-reference types to be gc roots. - // if type is types.EjsValue - // // EjsValues are rooted - // this.createCall this.llvm_intrinsics.gcroot(), [(ir.createPointerCast alloca, types.Int8Pointer.pointerTo(), 'rooted_alloca'), consts.Null types.Int8Pointer], '' - - ir.setInsertPoint(saved_insert_point); - return alloca; - } - - createAllocas(func, ids, scope) { - let allocas = []; - let new_allocas = []; - - // the allocas are always allocated in the function entry_bb so the mem2reg opt pass can regenerate the ssa form for us - let saved_insert_point = ir.getInsertBlock(); - ir.setInsertPointStartBB(func.entry_bb); - - let j = 0; - for (let i = 0, e = ids.length; i < e; i++) { - let name = ids[i].id.name; - if (!scope.has(name)) { - allocas[j] = ir.createAlloca(types.EjsValue, `local_${name}`); - allocas[j].setAlignment(8); - scope.set(name, allocas[j]); - new_allocas[j] = true; - } else { - allocas[j] = scope.get(name); - new_allocas[j] = false; - } - j = j + 1; - } - - // reinstate the IRBuilder to its previous insert point so we can insert the actual initializations - ir.setInsertPoint(saved_insert_point); - - return { allocas: allocas, new_allocas: new_allocas }; - } - - createPropertyStore(obj, prop, rhs, computed) { - if (computed) { - // we store obj[prop], prop can be any value - return this.createCall( - this.ejs_runtime.object_setprop, - [obj, this.visit(prop), rhs], - "propstore_computed" - ); - } else { - var pname; - - // we store obj.prop, prop is an id - if (prop.type === b.Identifier) pname = prop.name; // prop.type is b.Literal - else pname = prop.value; - - let c = this.getAtom(pname); - - debug.log(() => `createPropertyStore ${obj}[${pname}]`); - - return this.createCall( - this.ejs_runtime.object_setprop, - [obj, c, rhs], - `propstore_${pname}` - ); - } - } - - createPropertyLoad(obj, prop, computed, canThrow = true) { - if (computed) { - // we load obj[prop], prop can be any value - let loadprop = this.visit(prop); - - if (this.options.record_types) - this.createCall( - this.ejs_runtime.record_getprop, - [consts.int32(this.genRecordId()), obj, loadprop], - "" - ); - - return this.createCall( - this.ejs_runtime.object_getprop, - [obj, loadprop], - "getprop_computed", - canThrow - ); - } else { - // we load obj.prop, prop is an id - let pname = this.getAtom(prop.name); - - if (this.options.record_types) - this.createCall( - this.ejs_runtime.record_getprop, - [consts.int32(this.genRecordId()), obj, pname], - "" - ); - - return this.createCall( - this.ejs_runtime.object_getprop, - [obj, pname], - `getprop_${prop.name}`, - canThrow - ); - } - } - - setDebugLoc(ast_node) { - if (!this.options.debug) return; - if (!ast_node || !ast_node.loc) return; - if (!this.currentFunction) return; - if (!this.currentFunction.debug_info) return; - - ir.setCurrentDebugLocation( - llvm.DebugLoc.get( - ast_node.loc.start.line, - ast_node.loc.start.column, - this.currentFunction.debug_info - ) - ); - } - - visit(n) { - this.setDebugLoc(n); - return super.visit(n); - } - - visitOrNull(n) { - return this.visit(n) || this.loadNullEjsValue(); - } - visitOrUndefined(n) { - return this.visit(n) || this.loadUndefinedEjsValue(); - } - - visitProgram(n) { - // by the time we make it here the program has been - // transformed so that there is nothing at the toplevel - // but function declarations. - for (let func of n.body) this.visit(func); - } - - visitBlock(n) { - let new_scope = new Map(); - - let iife_dest_bb = null; - let iife_rv = null; - - if (n.fromIIFE) { - let insertBlock = ir.getInsertBlock(); - let insertFunc = insertBlock.parent; - - iife_dest_bb = new llvm.BasicBlock("iife_dest", insertFunc); - iife_rv = n.ejs_iife_rv; - } - - this.iifeStack.push({ iife_rv, iife_dest_bb }); - - this.visitWithScope(new_scope, n.body); - - this.iifeStack.pop(); - if (iife_dest_bb) { - ir.createBr(iife_dest_bb); - ir.setInsertPoint(iife_dest_bb); - let rv = this.createEjsValueLoad( - this.findIdentifierInScope(iife_rv.name), - "%iife_rv_load" - ); - return rv; - } else { - return n; - } - } - - visitSwitch(n) { - let insertBlock = ir.getInsertBlock(); - let insertFunc = insertBlock.parent; - - // find the default: case first - let defaultCase = null; - for (let _case of n.cases) { - if (!_case.test) { - defaultCase = _case; - break; - } - } - - // for each case, create 2 basic blocks - for (let _case of n.cases) { - _case.bb = new llvm.BasicBlock("case_bb", insertFunc); - if (_case !== defaultCase) - _case.dest_check = new llvm.BasicBlock("case_dest_check_bb", insertFunc); - } - - let merge_bb = new llvm.BasicBlock("switch_merge", insertFunc); - - let discr = this.visit(n.discriminant); - - let case_checks = []; - for (let _case of n.cases) { - if (defaultCase !== _case) - case_checks.push({ - test: _case.test, - dest_check: _case.dest_check, - body: _case.bb, - }); - } - - case_checks.push({ - dest_check: defaultCase ? defaultCase.bb : merge_bb, - }); - - this.doInsideExitableScope(new SwitchExitableScope(merge_bb), () => { - // insert all the code for the tests - ir.createBr(case_checks[0].dest_check); - ir.setInsertPoint(case_checks[0].dest_check); - for (let casenum = 0; casenum < case_checks.length - 1; casenum++) { - let test = this.visit(case_checks[casenum].test); - let eqop = this.ejs_binops["==="]; - - this.setDebugLoc(test); - let discTest = this.createCall(eqop, [discr, test], "test", !eqop.doesNotThrow); - - let disc_cmp, disc_truthy; - - if (discTest._ejs_returns_ejsval_bool) { - disc_cmp = this.createEjsvalICmpEq( - discTest, - consts.ejsval_false(this.triple.pointerSize() === 32) - ); - } else { - disc_truthy = this.createCall( - this.ejs_runtime.truthy, - [discTest], - "disc_truthy" - ); - disc_cmp = ir.createICmpEq(disc_truthy, consts.False(), "disccmpresult"); - } - ir.createCondBr( - disc_cmp, - case_checks[casenum + 1].dest_check, - case_checks[casenum].body - ); - ir.setInsertPoint(case_checks[casenum + 1].dest_check); - } - - let case_bodies = []; - - // now insert all the code for the case consequents - for (let _case of n.cases) - case_bodies.push({ - bb: _case.bb, - consequent: _case.consequent, - }); - - case_bodies.push({ bb: merge_bb }); - - for (let casenum = 0; casenum < case_bodies.length - 1; casenum++) { - ir.setInsertPoint(case_bodies[casenum].bb); - case_bodies[casenum].consequent.forEach((consequent) => { - this.visit(consequent); - }); - - ir.createBr(case_bodies[casenum + 1].bb); - } - - ir.setInsertPoint(merge_bb); - }); - - return merge_bb; - } - - visitCase() { - throw new Error("we shouldn't get here, case statements are handled in visitSwitch"); - } - - visitLabeledStatement(n) { - if ( - n.body.type === b.ForInStatement || - n.body.type === b.ForStatement || - n.body.type === b.ForOfStatement || - n.body.type === b.DoWhileStatement || - n.body.type === b.WhileStatement - ) { - // loops are handled by the individual loop statement visit functions - n.body.label = n.label.name; - return this.visit(n.body); - } - - let insertBlock = ir.getInsertBlock(); - let insertFunc = insertBlock.parent; - - let labeled_bb = new llvm.BasicBlock("labeled_bb", insertFunc); - let merge_bb = new llvm.BasicBlock("labeled_merge_bb", insertFunc); - - ir.createBr(labeled_bb); - - this.doInsideExitableScope( - new LabeledStatementExitableScope(n.label.name, merge_bb), - () => { - this.doInsideBBlock(labeled_bb, () => { - this.visit(n.body); - ir.createBr(merge_bb); - }); - } - ); - - ir.setInsertPoint(merge_bb); - return merge_bb; - } - - visitBreak(n) { - return ExitableScope.scopeStack.exitAft(true, n.label && n.label.name); - } - - visitContinue(n) { - if (n.label && n.label.name) - return LoopExitableScope.findLabeledOrFinally(n.label.name).exitFore(); - else return LoopExitableScope.findLoopOrFinally().exitFore(); - } - - generateCondBr(exp, then_bb, else_bb) { - let cmp, exp_value; - if (exp.type === b.Literal && typeof exp.value === "boolean") { - cmp = consts.int1(exp.value ? 0 : 1); // we check for false below, so the then/else branches get swapped - } else { - exp_value = this.visit(exp); - if (exp_value._ejs_returns_ejsval_bool) { - cmp = this.createEjsvalICmpEq( - exp_value, - consts.ejsval_false(this.triple.pointerSize() === 32), - "cmpresult" - ); - } else if (exp_value._ejs_returns_native_bool) { - cmp = ir.createSelect(exp_value, consts.int1(0), consts.int1(1), "invert_check"); - } else { - let cond_truthy = this.createCall( - this.ejs_runtime.truthy, - [exp_value], - "cond_truthy" - ); - cmp = ir.createICmpEq(cond_truthy, consts.False(), "cmpresult"); - } - } - ir.createCondBr(cmp, else_bb, then_bb); - return exp_value; - } - - visitFor(n) { - let insertBlock = ir.getInsertBlock(); - let insertFunc = insertBlock.parent; - - let init_bb = new llvm.BasicBlock("for_init", insertFunc); - let test_bb = new llvm.BasicBlock("for_test", insertFunc); - let body_bb = new llvm.BasicBlock("for_body", insertFunc); - let update_bb = new llvm.BasicBlock("for_update", insertFunc); - let merge_bb = new llvm.BasicBlock("for_merge", insertFunc); - - ir.createBr(init_bb); - - this.doInsideBBlock(init_bb, () => { - this.visit(n.init); - ir.createBr(test_bb); - }); - - this.doInsideBBlock(test_bb, () => { - if (n.test) this.generateCondBr(n.test, body_bb, merge_bb); - else ir.createBr(body_bb); - }); - - this.doInsideExitableScope(new LoopExitableScope(n.label, update_bb, merge_bb), () => { - this.doInsideBBlock(body_bb, () => { - this.visit(n.body); - ir.createBr(update_bb); - }); - - this.doInsideBBlock(update_bb, () => { - this.visit(n.update); - ir.createBr(test_bb); - }); - }); - - ir.setInsertPoint(merge_bb); - return merge_bb; - } - - visitDo(n) { - let insertBlock = ir.getInsertBlock(); - let insertFunc = insertBlock.parent; - - let body_bb = new llvm.BasicBlock("do_body", insertFunc); - let test_bb = new llvm.BasicBlock("do_test", insertFunc); - let merge_bb = new llvm.BasicBlock("do_merge", insertFunc); - - ir.createBr(body_bb); - - this.doInsideExitableScope(new LoopExitableScope(n.label, test_bb, merge_bb), () => { - this.doInsideBBlock(body_bb, () => { - this.visit(n.body); - ir.createBr(test_bb); - }); - this.doInsideBBlock(test_bb, () => { - this.generateCondBr(n.test, body_bb, merge_bb); - }); - }); - - ir.setInsertPoint(merge_bb); - return merge_bb; - } - - visitWhile(n) { - let insertBlock = ir.getInsertBlock(); - let insertFunc = insertBlock.parent; - - let while_bb = new llvm.BasicBlock("while_start", insertFunc); - let body_bb = new llvm.BasicBlock("while_body", insertFunc); - let merge_bb = new llvm.BasicBlock("while_merge", insertFunc); - - ir.createBr(while_bb); - - this.doInsideBBlock(while_bb, () => { - this.generateCondBr(n.test, body_bb, merge_bb); - }); - - this.doInsideExitableScope(new LoopExitableScope(n.label, while_bb, merge_bb), () => { - this.doInsideBBlock(body_bb, () => { - this.visit(n.body); - ir.createBr(while_bb); - }); - }); - - ir.setInsertPoint(merge_bb); - return merge_bb; - } - - visitForIn(n) { - let insertBlock = ir.getInsertBlock(); - let insertFunc = insertBlock.parent; - - let iterator = this.createCall( - this.ejs_runtime.prop_iterator_new, - [this.visit(n.right)], - "iterator" - ); - - let lhs; - // make sure we get an alloca if there's a 'var' - if (n.left[0]) { - this.visit(n.left); - lhs = n.left[0].declarations[0].id; - } else { - lhs = n.left; - } - - let forin_bb = new llvm.BasicBlock("forin_start", insertFunc); - let body_bb = new llvm.BasicBlock("forin_body", insertFunc); - let merge_bb = new llvm.BasicBlock("forin_merge", insertFunc); - - ir.createBr(forin_bb); - - this.doInsideExitableScope(new LoopExitableScope(n.label, forin_bb, merge_bb), () => { - // forin_bb: - // moreleft = prop_iterator_next (iterator, true) - // if moreleft === false - // goto merge_bb - // else - // goto body_bb - // - this.doInsideBBlock(forin_bb, () => { - let moreleft = this.createCall( - this.ejs_runtime.prop_iterator_next, - [iterator, consts.True()], - "moreleft" - ); - let cmp = ir.createICmpEq(moreleft, consts.False(), "cmpmoreleft"); - ir.createCondBr(cmp, merge_bb, body_bb); - }); - - // body_bb: - // current = prop_iteratorcurrent (iterator) - // *lhs = current - // - // goto forin_bb - this.doInsideBBlock(body_bb, () => { - let current = this.createCall( - this.ejs_runtime.prop_iterator_current, - [iterator], - "iterator_current" - ); - this.storeValueInDest(current, lhs); - this.visit(n.body); - ir.createBr(forin_bb); - }); - }); - - // merge_bb: - // - ir.setInsertPoint(merge_bb); - return merge_bb; - } - - visitForOf() { - throw new Error( - "internal compiler error. for..of statements should have been transformed away by this point." - ); - } - - visitUpdateExpression(n) { - let result = this.createAlloca(this.currentFunction, types.EjsValue, "%update_result"); - let argument = this.visit(n.argument); - - let one = this.loadDoubleEjsValue(1); - - if (!n.prefix) { - // postfix updates store the argument before the op - ir.createStore(argument, result); - } - - // argument = argument $op 1 - let update_op = this.ejs_binops[n.operator === "++" ? "+" : "-"]; - let temp = this.createCall( - update_op, - [argument, one], - "update_temp", - !update_op.doesNotThrow - ); - - this.storeValueInDest(temp, n.argument); - - // return result - if (n.prefix) { - argument = this.visit(n.argument); - // prefix updates store the argument after the op - ir.createStore(argument, result); - } - return this.createEjsValueLoad(result, "%update_result_load"); - } - - visitConditionalExpression(n) { - return this.visitIfOrCondExp(n, true); - } - - visitIf(n) { - return this.visitIfOrCondExp(n, false); - } - - visitIfOrCondExp(n, load_result) { - let cond_val; - - if (load_result) - cond_val = this.createAlloca(this.currentFunction, types.EjsValue, "%cond_val"); - - let insertBlock = ir.getInsertBlock(); - let insertFunc = insertBlock.parent; - - let then_bb = new llvm.BasicBlock("then", insertFunc); - let else_bb; - if (n.alternate) else_bb = new llvm.BasicBlock("else", insertFunc); - let merge_bb = new llvm.BasicBlock("merge", insertFunc); - - this.generateCondBr(n.test, then_bb, else_bb ? else_bb : merge_bb); - - this.doInsideBBlock(then_bb, () => { - let then_val = this.visit(n.consequent); - if (load_result) ir.createStore(then_val, cond_val); - ir.createBr(merge_bb); - }); - - if (n.alternate) { - this.doInsideBBlock(else_bb, () => { - let else_val = this.visit(n.alternate); - if (load_result) ir.createStore(else_val, cond_val); - ir.createBr(merge_bb); - }); - } - - ir.setInsertPoint(merge_bb); - if (load_result) return this.createEjsValueLoad(cond_val, "cond_val_load"); - else return merge_bb; - } - - visitReturn(n) { - if (this.iifeStack.top.iife_rv) { - // if we're inside an IIFE, convert the return statement into a store to the iife_rv alloca + a branch to the iife's dest bb - if (n.argument) - ir.createStore( - this.visit(n.argument), - this.findIdentifierInScope(this.iifeStack.top.iife_rv.name) - ); - ir.createBr(this.iifeStack.top.iife_dest_bb); - } else { - // otherwise generate an llvm IR ret - let rv = this.visitOrUndefined(n.argument); - - if (this.finallyStack.length > 0) { - if (!this.currentFunction.returnValueAlloca) - this.currentFunction.returnValueAlloca = this.createAlloca( - this.currentFunction, - types.EjsValue, - "returnValue" - ); - ir.createStore(rv, this.currentFunction.returnValueAlloca); - ir.createStore( - consts.int32(ExitableScope.REASON_RETURN), - this.currentFunction.cleanup_reason - ); - ir.createBr(this.finallyStack[0]); - } else { - let return_alloca = this.createAlloca( - this.currentFunction, - types.EjsValue, - "return_alloca" - ); - ir.createStore(rv, return_alloca); - - this.createRet(this.createEjsValueLoad(return_alloca, "return_load")); - } - } - } - - visitVariableDeclaration(n) { - if (n.kind === "var") - throw new Error( - "internal compiler error. var declarations should have been transformed to lets by this point." - ); - - let scope = this.scope_stack.top; - - let { allocas, new_allocas } = this.createAllocas( - this.currentFunction, - n.declarations, - scope - ); - for (let i = 0, e = n.declarations.length; i < e; i++) { - if (!n.declarations[i].init) { - // there was not an initializer. we only store undefined - // if the alloca is newly allocated. - if (new_allocas[i]) { - let initializer = this.visitOrUndefined(n.declarations[i].init); - ir.createStore(initializer, allocas[i]); - } - } else { - let initializer = this.visitOrUndefined(n.declarations[i].init); - ir.createStore(initializer, allocas[i]); - } - } - } - - visitMemberExpression(n) { - return this.createPropertyLoad(this.visit(n.object), n.property, n.computed); - } - - storeValueInDest(rhvalue, lhs) { - if (lhs.type === b.Identifier) { - let dest = this.findIdentifierInScope(lhs.name); - let result; - if (dest) result = ir.createStore(rhvalue, dest); - else result = this.storeGlobal(lhs, rhvalue); - return result; - } else if (lhs.type === b.MemberExpression) { - return this.createPropertyStore( - this.visit(lhs.object), - lhs.property, - rhvalue, - lhs.computed - ); - } else if (is_intrinsic(lhs, "%slot")) { - return ir.createStore(rhvalue, this.handleSlotRef(lhs)); - } else if (is_intrinsic(lhs, "%getLocal")) { - return ir.createStore(rhvalue, this.findIdentifierInScope(lhs.arguments[0].name)); - } else if (is_intrinsic(lhs, "%getGlobal")) { - let gname = lhs.arguments[0].name; - - return this.createCall( - this.ejs_runtime.global_setprop, - [this.getAtom(gname), rhvalue], - `globalpropstore_${lhs.arguments[0].name}` - ); - } else { - throw new Error(`unhandled lhs ${escodegenerate(lhs)}`); - } - } - - visitAssignmentExpression(n) { - let lhs = n.left; - let rhs = n.right; - - let rhvalue = this.visit(rhs); - - if (n.operator.length === 2) - throw new Error( - `binary assignment operators '${n.operator}' should not exist at this point` - ); - - if (this.options.record_types) - this.createCall( - this.ejs_runtime.record_assignment, - [consts.int32(this.genRecordId()), rhvalue], - "" - ); - this.storeValueInDest(rhvalue, lhs); - - // we need to visit lhs after the store so that we load the value, but only if it's used - if (!n.result_not_used) return rhvalue; - } - - visitFunction(n) { - if (!n.toplevel) - debug.log( - () => - ` function ${n.ir_name} at ${this.filename}:${ - n.loc ? n.loc.start.line : "" - }` - ); - - // save off the insert point so we can get back to it after generating this function - let insertBlock = ir.getInsertBlock(); - - for (let param of n.formal_params) { - if (param.type !== b.Identifier) - throw new Error("formal parameters should only be identifiers by this point"); - } - - // XXX this methods needs to be augmented so that we can pass actual types (or the builtin args need - // to be reflected in jsllvm.cpp too). maybe we can pass the names to this method and it can do it all - // there? - - let ir_func = n.ir_func; - let ir_args = n.ir_func.args; - debug.log(""); - //debug.log -> `ir_func = ${ir_func}` - - //debug.log -> `param ${param.llvm_type} ${param.name}` for param in n.formal_params - - this.currentFunction = ir_func; - - // we need to do this here as well, since otherwise the allocas and stores we create below for our parameters - // could be accidentally attributed to the previous @currentFunction (the last location we set). - this.setDebugLoc(n); - - // Create a new basic block to start insertion into. - let entry_bb = new llvm.BasicBlock("entry", ir_func); - - ir.setInsertPoint(entry_bb); - - let new_scope = new Map(); - - // we save off the top scope and entry_bb of the function so that we can hoist vars there - ir_func.topScope = new_scope; - ir_func.entry_bb = entry_bb; - - ir_func.literalAllocas = Object.create(null); - - let allocas = []; - - // create allocas for the builtin args - for (let param of n.params) { - let alloca = ir.createAlloca(param.llvm_type, `local_${param.name}`); - alloca.setAlignment(8); - new_scope.set(param.name, alloca); - allocas.push(alloca); - } - - /* - // now create allocas for the formal parameters - let first_formal_index = allocas.length; - for (let param of n.formal_params) { - let alloca = this.createAlloca(this.currentFunction, types.EjsValue, `local_${param.name}`); - new_scope.set(param.name, alloca); - allocas.push(alloca); - } -*/ - - debug.log(() => { - allocas.map((alloca) => `alloca ${alloca}`).join("\n"); - }); - - // now store the arguments onto the stack - for (let i = 0, e = n.params.length; i < e; i++) { - var store = ir.createStore(ir_args[i], allocas[i]); - debug.log(() => `store ${store} *builtin`); - } - - let body_bb = new llvm.BasicBlock("body", ir_func); - ir.setInsertPoint(body_bb); - - //this.createCall this.ejs_runtime.log, [consts.string(ir, `entering ${n.ir_name}`)], '' - - this.iifeStack = new Stack(); - - this.finallyStack = []; - - this.visitWithScope(new_scope, [n.body]); - - // XXX more needed here - this lacks all sorts of control flow stuff. - // Finish off the function. - this.createRet(this.loadUndefinedEjsValue()); - - if (n.toplevel) { - this.resolve_modules_bb = new llvm.BasicBlock("resolve_modules", ir_func); - this.toplevel_body_bb = body_bb; - this.toplevel_function = ir_func; - - // branch to the resolve_modules_bb from our entry_bb, but only in the toplevel function - ir.setInsertPoint(entry_bb); - ir.createBr(this.resolve_modules_bb); - } else { - // branch to the body_bb from our entry_bb - ir.setInsertPoint(entry_bb); - ir.createBr(body_bb); - } - - this.currentFunction = null; - - ir.setInsertPoint(insertBlock); - - return ir_func; - } - - createRet(x) { - //this.createCall this.ejs_runtime.log, [consts.string(ir, `leaving ${this.currentFunction.name}`)], '' - return this.abi.createRet(this.currentFunction, x); - } - - visitUnaryExpression(n) { - debug.log(() => `operator = '${n.operator}'`); - - let builtin = `unop${n.operator}`; - let callee = this.ejs_runtime[builtin]; - - if (n.operator === "delete") { - if (n.argument.type !== b.MemberExpression) throw "unhandled delete syntax"; - - let fake_literal = { - type: b.Literal, - value: n.argument.property.name, - raw: `'${n.argument.property.name}'`, - }; - return this.createCall( - callee, - [this.visitOrNull(n.argument.object), this.visit(fake_literal)], - "result" - ); - } else if (n.operator === "!") { - let arg_value = this.visitOrNull(n.argument); - if ( - this.opencode_intrinsics.unaryNot && - this.triple.pointerSize() === 64 && - arg_value._ejs_returns_ejsval_bool - ) { - let cmp = this.createEjsvalICmpEq( - arg_value, - consts.ejsval_true(false), - "cmpresult" - ); - return this.createEjsBoolSelect(cmp, true); - } else { - return this.createCall(callee, [arg_value], "result"); - } - } else { - if (!callee) { - throw new Error(`Internal error: unary operator '${n.operator}' not implemented`); - } - return this.createCall(callee, [this.visitOrNull(n.argument)], "result"); - } - } - - visitSequenceExpression(n) { - let rv = null; - for (let exp of n.expressions) rv = this.visit(exp); - return rv; - } - - visitBinaryExpression(n) { - debug.log(() => `operator = '${n.operator}'`); - let callee = this.ejs_binops[n.operator]; - - if (!callee) throw new Error(`Internal error: unhandled binary operator '${n.operator}'`); - - let left_visited = this.visit(n.left); - let right_visited = this.visit(n.right); - - if (this.options.record_types) - this.createCall( - this.ejs_runtime.record_binop, - [ - consts.int32(this.genRecordId()), - consts.string(ir, n.operator), - left_visited, - right_visited, - ], - "" - ); - - // call the actual runtime binaryop method - return this.createCall( - callee, - [left_visited, right_visited], - `result_${n.operator}`, - !callee.doesNotThrow - ); - } - - visitLogicalExpression(n) { - debug.log(() => `operator = '${n.operator}'`); - let result = this.createAlloca( - this.currentFunction, - types.EjsValue, - `result_${n.operator}` - ); - - let insertBlock = ir.getInsertBlock(); - let insertFunc = insertBlock.parent; - - let left_bb = new llvm.BasicBlock("cond_left", insertFunc); - let right_bb = new llvm.BasicBlock("cond_right", insertFunc); - let merge_bb = new llvm.BasicBlock("cond_merge", insertFunc); - - // we invert the test here - check if the condition is false/0 - let left_visited = this.generateCondBr(n.left, left_bb, right_bb); - - this.doInsideBBlock(left_bb, () => { - // inside the else branch, left was truthy - if (n.operator === "||") - // for || we short circuit out here - ir.createStore(left_visited, result); - else if (n.operator === "&&") - // for && we evaluate the second and store it - ir.createStore(this.visit(n.right), result); - else throw "Internal error 99.1"; - ir.createBr(merge_bb); - }); - - this.doInsideBBlock(right_bb, () => { - // inside the then branch, left was falsy - if (n.operator === "||") - // for || we evaluate the second and store it - ir.createStore(this.visit(n.right), result); - else if (n.operator === "&&") - // for && we short circuit out here - ir.createStore(left_visited, result); - else throw "Internal error 99.1"; - ir.createBr(merge_bb); - }); - - ir.setInsertPoint(merge_bb); - return this.createEjsValueLoad(result, `result_${n.operator}_load`); - } - - visitArgsForCall(callee, pullThisFromArg0, args) { - args = args.slice(); - let argv = []; - - if (callee.takes_builtins) { - let thisArg, closure; - if (pullThisFromArg0 && args[0].type === b.MemberExpression) { - thisArg = this.visit(args[0].object); - closure = this.createPropertyLoad(thisArg, args[0].property, args[0].computed); - } else { - thisArg = this.loadUndefinedEjsValue(); - closure = this.visit(args[0]); - } - - let this_alloca = this.createAlloca( - this.currentFunction, - types.EjsValue, - "this_alloca" - ); - ir.createStore(thisArg, this_alloca, "this_alloca_store"); - - args.shift(); - - argv.push(closure); // %closure - argv.push(this_alloca); // %this - argv.push(consts.int32(args.length)); // %argc - - let args_length = args.length; - if (args_length > 0) { - const scratchAreaType = llvm.ArrayType.get( - types.EjsValue, - this.currentFunction.scratch_length - ); - - for (let i = 0; i < args_length; i++) { - args[i] = this.visitOrNull(args[i]); - } - for (let i = 0; i < args_length; i++) { - let gep = ir.createGetElementPointer( - scratchAreaType, - this.currentFunction.scratch_area, - [consts.int32(0), consts.int64(i)], - `arg_gep_${i}` - ); - ir.createStore(args[i], gep, `argv[${i}]-store`); - } - - let argsCast = ir.createGetElementPointer( - scratchAreaType, - this.currentFunction.scratch_area, - [consts.int32(0), consts.int64(0)], - "call_args_load" - ); - - argv.push(argsCast); - } else { - argv.push(consts.Null(types.EjsValue.pointerTo())); - } - - argv.push(this.loadUndefinedEjsValue()); // %newTarget = undefined - } else { - for (let a of args) argv.push(this.visitOrNull(a)); - } - - return argv; - } - - debugLog(str) { - if (this.options.debug_level > 0) - this.createCall(this.ejs_runtime.log, [consts.string(ir, str)], ""); - } - - visitArgsForConstruct(callee, args, this_loc, newTarget_loc) { - args = args.slice(); - let argv = []; - // constructors are always .takes_builtins, so we can skip the other case - // - - let ctor = this.visit(args[0]); - args.shift(); - - argv.push(ctor); // %closure - argv.push(this_loc); // %this - argv.push(consts.int32(args.length)); // %argc - - if (args.length > 0) { - const scratchAreaType = llvm.ArrayType.get( - types.EjsValue, - this.currentFunction.scratch_length - ); - let visited = []; - for (let a of args) visited.push(this.visitOrNull(a)); - - visited.forEach((a, i) => { - let gep = ir.createGetElementPointer( - scratchAreaType, - this.currentFunction.scratch_area, - [consts.int32(0), consts.int64(i)], - `arg_gep_${i}` - ); - ir.createStore(a, gep, `argv[${i}]-store`); - }); - - let argsCast = ir.createGetElementPointer( - scratchAreaType, - this.currentFunction.scratch_area, - [consts.int32(0), consts.int64(0)], - "call_args_load" - ); - argv.push(argsCast); - } else { - argv.push(consts.Null(types.EjsValue.pointerTo())); - } - - argv.push(newTarget_loc || ctor); // %newTarget = ctor - - return argv; - } - - visitCallExpression(n) { - debug.log(() => `visitCall ${JSON.stringify(n)}`); - debug.log(() => ` arguments length = ${n.arguments.length}`); - - debug.log(() => { - return n.arguments - .map((a, i) => ` arguments[${i}] = ${JSON.stringify(a)}`) - .join(""); - }); - - let unescapedName = n.callee.name.slice(1); - let intrinsicHandler = this.ejs_intrinsics[unescapedName]; - if (!intrinsicHandler) - throw new Error( - `Internal error: callee should not be null in visitCallExpression (callee = '${n.callee.name}', arguments = ${n.arguments.length})` - ); - - return intrinsicHandler.call(this, n, this.opencode_intrinsics[unescapedName]); - } - - visitThisExpression() { - debug.log("visitThisExpression"); - return this.createEjsValueLoad( - this.createLoad( - types.EjsValue.pointerTo(), - this.findIdentifierInScope("%this"), - "load_this_ptr" - ), - "load_this" - ); - } - - visitSpreadElement() { - throw new Error("halp"); - } - - visitIdentifier(n) { - let rv; - debug.log(() => `identifier ${n.name}`); - let val = n.name; - - let source = this.findIdentifierInScope(val); - if (source) { - debug.log(() => `found identifier in scope, at ${source}`); - rv = this.createEjsValueLoad(source, `load_${val}`); - return rv; - } - - // special handling of the arguments object here, so we - // only initialize/create it if the function is - // actually going to use it. - if (val === "arguments") { - let arguments_alloca = this.createAlloca( - this.currentFunction, - types.EjsValue, - "local_arguments_object" - ); - let saved_insert_point = ir.getInsertBlock(); - ir.setInsertPoint(this.currentFunction.entry_bb); - - let load_argc = this.createLoad( - types.Int32, - this.currentFunction.topScope.get("%argc"), - "argc_load" - ); - let load_args = this.createLoad( - types.EjsValue.pointerTo(), - this.currentFunction.topScope.get("%args"), - "args_load" - ); - - let args_new = this.ejs_runtime.arguments_new; - let arguments_object = this.createCall( - args_new, - [load_argc, load_args], - "argstmp", - !args_new.doesNotThrow - ); - ir.createStore(arguments_object, arguments_alloca); - this.currentFunction.topScope.set("arguments", arguments_alloca); - - ir.setInsertPoint(saved_insert_point); - return this.createEjsValueLoad(arguments_alloca, "load_arguments"); - } - - rv = null; - debug.log(() => `calling getFunction for ${val}`); - rv = this.module.getFunction(val); - - if (!rv) { - debug.log(() => `Symbol '${val}' not found in current scope`); - rv = this.loadGlobal(n); - } - - debug.log(() => `returning ${rv}`); - return rv; - } - - visitObjectExpression(n) { - let obj_proto = ir.createLoad( - types.EjsValue, - this.ejs_globals.Object_prototype, - "load_objproto" - ); - let object_create = this.ejs_runtime.object_create; - let obj = this.createCall( - object_create, - [obj_proto], - "objtmp", - !object_create.doesNotThrow - ); - - let accessor_map = new Map(); - - // gather all properties so we can emit get+set as a single call to define_accessor_prop. - for (let property of n.properties) { - if (property.kind === "get" || property.kind === "set") { - if (!accessor_map.has(property.key)) accessor_map.set(property.key, new Map()); - if (accessor_map.get(property.key).has(property.kind)) - throw new SyntaxError( - `a '${property.kind}' method for '${escodegenerate( - property.key - )}' has already been defined.` - ); - if (accessor_map.get(property.key).has("init")) - throw new SyntaxError( - `${property.key.loc.start.line}: property name ${escodegenerate( - property.key - )} appears once in object literal.` - ); - } else if (property.kind === "init") { - if (accessor_map.get(property.key)) - throw new SyntaxError( - `${property.key.loc.start.line}: property name ${escodegenerate( - property.key - )} appears once in object literal.` - ); - accessor_map.set(property.key, new Map()); - } else { - throw new Error(`unrecognized property kind '${property.kind}'`); - } - - if (property.computed) { - accessor_map.get(property.key).set("computed", true); - } - accessor_map.get(property.key).set(property.kind, property); - } - - accessor_map.forEach((prop_map, propkey) => { - // XXX we need something like this line below to handle computed properties, but those are broken at the moment - //key = if property.key.type is Identifier then this.getAtom property.key.name else this.visit property.key - - if (prop_map.has("computed")) propkey = this.visit(propkey); - else if (propkey.type == b.Literal) propkey = this.getAtom(String(propkey.value)); - else if (propkey.type === b.Identifier) propkey = this.getAtom(propkey.name); - - if (prop_map.has("init")) { - let val = this.visit(prop_map.get("init").value); - this.createCall( - this.ejs_runtime.object_define_value_prop, - [obj, propkey, val, consts.int32(0x77)], - `define_value_prop_${propkey}` - ); - } else { - let getter = prop_map.get("get"); - let setter = prop_map.get("set"); - - let get_method = getter ? this.visit(getter.value) : this.loadUndefinedEjsValue(); - let set_method = setter ? this.visit(setter.value) : this.loadUndefinedEjsValue(); - - this.createCall( - this.ejs_runtime.object_define_accessor_prop, - [obj, propkey, get_method, set_method, consts.int32(0x19)], - `define_accessor_prop_${propkey}` - ); - } - }); - - return obj; - } - - visitArrayExpression(n) { - let force_fill = false; - // if there are holes, we need to fill the array at allocation time. - // FIXME(toshok) we could just as easily have the compiler emit code to initialize the holes as well, right? - for (let el of n.elements) { - if (el == null) { - force_fill = true; - break; - } - } - - let obj = this.createCall( - this.ejs_runtime.array_new, - [consts.int64(n.elements.length), consts.bool(force_fill)], - "arrtmp", - !this.ejs_runtime.array_new.doesNotThrow - ); - let i = 0; - for (let el of n.elements) { - // don't create property stores for array holes - if (el == null) continue; - - let val = this.visit(el); - let index = { type: b.Literal, value: i }; - this.createPropertyStore(obj, index, val, true); - i = i + 1; - } - return obj; - } - - visitExpressionStatement(n) { - n.expression.result_not_used = true; - return this.visit(n.expression); - } - - generateUCS2(id, jsstr) { - let ucsArrayType = llvm.ArrayType.get(types.JSChar, jsstr.length + 1); - let array_data = []; - for (let i = 0, e = jsstr.length; i < e; i++) - array_data.push(consts.jschar(jsstr.charCodeAt(i))); - array_data.push(consts.jschar(0)); - let array = llvm.ConstantArray.get(ucsArrayType, array_data); - let arrayglobal = new llvm.GlobalVariable( - this.module, - ucsArrayType, - `ucs2-${id}`, - array, - false - ); - arrayglobal.setAlignment(8); - return arrayglobal; - } - - generateEJSPrimString(id) { - let strglobal = new llvm.GlobalVariable( - this.module, - types.EjsPrimString, - `primstring-${id}`, - llvm.Constant.getAggregateZero(types.EjsPrimString), - false - ); - strglobal.setAlignment(8); - return strglobal; - } - - generateEJSValueForString(id) { - let name = `ejsval-${id}`; - let strglobal = new llvm.GlobalVariable( - this.module, - types.EjsValue, - name, - llvm.Constant.getAggregateZero(types.EjsValue), - false - ); - strglobal.setAlignment(8); - let val = this.module.getOrInsertGlobal(name, types.EjsValue); - val.setAlignment(8); - return val; - } - - addStringLiteralInitialization(name, ucs2, primstr, val, len) { - let saved_insert_point = ir.getInsertBlock(); - - ir.setInsertPointStartBB(this.literalInitializationBB); - - let saved_debug_loc; - if (this.options.debug) { - saved_debug_loc = ir.getCurrentDebugLocation(); - ir.setCurrentDebugLocation( - llvm.DebugLoc.get(0, 0, this.literalInitializationDebugInfo) - ); - } - - let strname = consts.string(ir, name); - - let arg0 = strname; - let arg1 = val; - let arg2 = primstr; - let arg3 = ir.createInBoundsGetElementPointer( - types.JSChar.pointerTo(), - ucs2, - [consts.int32(0), consts.int32(0)], - "ucs2" - ); - - ir.createCall( - this.ejs_runtime.init_string_literal.type, - this.ejs_runtime.init_string_literal, - [arg0, arg1, arg2, arg3, consts.int32(len)], - "" - ); - ir.setInsertPoint(saved_insert_point); - if (this.options.debug) ir.setCurrentDebugLocation(saved_debug_loc); - } - - getAtom(str) { - // check if it's an atom (a runtime library constant) first of all - if (hasOwn.call(this.ejs_atoms, str)) - return this.createEjsValueLoad(this.ejs_atoms[str], `${str}_atom_load`); - - // if it's not, we create a constant and embed it in this module - if (!this.module_atoms.has(str)) { - let literalId = this.idgen(); - let ucs2_data = this.generateUCS2(literalId, str); - let primstring = this.generateEJSPrimString(literalId, str.length); - let ejsval = this.generateEJSValueForString(str); - this.module_atoms.set(str, ejsval); - this.addStringLiteralInitialization(str, ucs2_data, primstring, ejsval, str.length); - } - - return this.createEjsValueLoad(this.module_atoms.get(str), "literal_load"); - } - - visitLiteral(n) { - // null literals, load _ejs_null - if (n.value === null) { - debug.log("literal: null"); - return this.loadNullEjsValue(); - } - - // undefined literals, load _ejs_undefined - if (n.value === undefined) { - debug.log("literal: undefined"); - return this.loadUndefinedEjsValue(); - } - - // string literals - if (typeof n.raw === "string" && (n.raw[0] === "'" || n.raw[0] === '"')) { - debug.log(() => `literal string: ${n.value}`); - - var strload = this.getAtom(n.value); - - strload.literal = n; - debug.log(() => `strload = ${strload}`); - return strload; - } - - // regular expression literals - if (typeof n.raw === "string" && n.raw[0] === "/") { - debug.log(() => `literal regexp: ${n.raw}`); - - let source = consts.string(ir, n.value.source); - let flags = consts.string( - ir, - `${n.value.global ? "g" : ""}${n.value.multiline ? "m" : ""}${ - n.value.ignoreCase ? "i" : "" - }` - ); - - let regexp_new_utf8 = this.ejs_runtime.regexp_new_utf8; - var regexpcall = this.createCall( - regexp_new_utf8, - [source, flags], - "regexptmp", - !regexp_new_utf8.doesNotThrow - ); - debug.log(() => `regexpcall = ${regexpcall}`); - return regexpcall; - } - - // number literals - if (typeof n.value === "number") { - debug.log(() => `literal number: ${n.value}`); - return this.loadDoubleEjsValue(n.value); - } - - // boolean literals - if (typeof n.value === "boolean") { - debug.log(() => `literal boolean: ${n.value}`); - return this.loadBoolEjsValue(n.value); - } - - throw `Internal error: unrecognized literal of type ${typeof n.value}`; - } - - createCall(callee, argv, callname, canThrow = true) { - // if we're inside a try block we have to use createInvoke, and pass two basic blocks: - // the normal block, which is basically this IR instruction's continuation - // the unwind block, where we land if the call throws an exception. - // - // Although for builtins we know won't throw, we can still use createCall. - let calltmp; - if (TryExitableScope.unwindStack.depth === 0 || callee.doesNotThrow || !canThrow) { - //ir.createCall this.ejs_runtime.log, [consts.string(ir, `calling ${callee.name}`)], '' - calltmp = this.abi.createCall( - this.currentFunction, - callee.type, - callee, - argv, - callname - ); - } else { - let normal_block = new llvm.BasicBlock("normal", this.currentFunction); - //ir.createCall this.ejs_runtime.log, [consts.string(ir, `invoking ${callee.name}`)], '' - calltmp = this.abi.createInvoke( - this.currentFunction, - callee.type, - callee, - argv, - normal_block, - TryExitableScope.unwindStack.top.getLandingPadBlock(), - callname - ); - // after we've made our call we need to change the insertion point to our continuation - ir.setInsertPoint(normal_block); - } - return calltmp; - } - - visitThrow(n) { - let arg = this.visit(n.argument); - this.createCall(this.ejs_runtime.throw, [arg], "", true); - return ir.createUnreachable(); - } - - visitTry(n) { - let insertBlock = ir.getInsertBlock(); - let insertFunc = insertBlock.parent; - - let finally_block = null; - let catch_block = null; - - // the alloca that stores the reason we ended up in the finally block - if (!this.currentFunction.cleanup_reason) - this.currentFunction.cleanup_reason = this.createAlloca( - this.currentFunction, - types.Int32, - "cleanup_reason" - ); - - // if we have a finally clause, create finally_block - if (n.finalizer) { - finally_block = new llvm.BasicBlock("finally_bb", insertFunc); - this.finallyStack.unshift(finally_block); - } - - // the merge bb where everything branches to after falling off the end of a catch/finally block - let merge_block = new llvm.BasicBlock("try_merge", insertFunc); - - let branch_target = finally_block ? finally_block : merge_block; - - let scope = new TryExitableScope( - this.currentFunction.cleanup_reason, - branch_target, - () => new llvm.BasicBlock("exception", insertFunc), - finally_block != null - ); - this.doInsideExitableScope(scope, () => { - scope.enterTry(); - this.visit(n.block); - - if (n.finalizer) this.finallyStack.shift(); - - // at the end of the try block branch to our branch_target (either the finally block or the merge block after the try{}) with REASON_FALLOFF - scope.exitAft(false); - scope.leaveTry(); - }); - - if (scope.landing_pad_block && n.handlers.length > 0) - catch_block = new llvm.BasicBlock("catch_bb", insertFunc); - - if (scope.landing_pad_block) { - // the scope's landingpad block is created if needed by this.createCall (using that function we pass in as the last argument to TryExitableScope's ctor.) - // if a try block includes no calls, there's no need for an landing pad block as nothing can throw, and we don't bother generating any code for the - // catch clause. - this.doInsideBBlock(scope.landing_pad_block, () => { - // XXX is it an error to have multiple catch handlers, as JS doesn't allow you to filter by type? - let clause_count = n.handlers.length > 0 ? 1 : 0; - - // XXX(llvm 3.8) - // let casted_personality = ir.createPointerCast(this.ejs_runtime.personality, types.Int8Pointer, 'personality'); - let caught_result = ir.createLandingPad( - types.EjsLandingPad, - clause_count, - "caught_result" - ); - caught_result.addClause( - ir.createPointerCast(this.ejs_runtime.exception_typeinfo, types.Int8Pointer, "") - ); - caught_result.setCleanup(true); - - let insertBlock = ir.getInsertBlock(); - let insertFunc = insertBlock.parent; - - if (!insertFunc.hasPersonality()) { - insertFunc.setPersonality( - ir.createPointerCast( - this.ejs_runtime.personality, - types.Int8Pointer, - "personality" - ) - ); - } - - let exception = ir.createExtractValue(caught_result, 0, "exception"); - - if (catch_block) ir.createBr(catch_block); - else if (finally_block) ir.createBr(finally_block); - else throw "this shouldn't happen. a try{} without either a catch{} or finally{}"; - - // if we have a catch clause, create catch_bb - if (n.handlers.length > 0) { - this.doInsideBBlock(catch_block, () => { - // call _ejs_begin_catch to return the actual exception - let catchval = this.beginCatch(exception); - - // create a new scope which maps the catch parameter name (the 'e' in 'try { } catch (e) { }') to catchval - let catch_scope = new Map(); - if (n.handlers[0].param && n.handlers[0].param.name) { - let catch_name = n.handlers[0].param.name; - let alloca = this.createAlloca( - this.currentFunction, - types.EjsValue, - `local_catch_${catch_name}` - ); - catch_scope.set(catch_name, alloca); - ir.createStore(catchval, alloca); - } - - if (n.finalizer) this.finallyStack.unshift(finally_block); - - this.doInsideExitableScope(scope, () => { - this.visitWithScope(catch_scope, [n.handlers[0]]); - }); - - // unsure about this one - we should likely call end_catch if another exception is thrown from the catch block? - this.endCatch(); - - if (n.finalizer) this.finallyStack.shift(); - - // at the end of the catch block branch to our branch_target (either the finally block or the merge block after the try{}) with REASON_FALLOFF - scope.exitAft(false); - }); - } - }); - } - - // Finally Block - if (n.finalizer) { - this.doInsideBBlock(finally_block, () => { - this.visit(n.finalizer); - - let cleanup_reason = this.createLoad( - types.Int32, - this.currentFunction.cleanup_reason, - "cleanup_reason_load" - ); - - let return_tramp = null; - if (this.currentFunction.returnValueAlloca) { - return_tramp = new llvm.BasicBlock("return_tramp", insertFunc); - this.doInsideBBlock(return_tramp, () => { - if (this.finallyStack.length > 0) { - ir.createStore( - consts.int32(ExitableScope.REASON_RETURN), - this.currentFunction.cleanup_reason - ); - ir.createBr(this.finallyStack[0]); - } else { - this.createRet( - this.createEjsValueLoad( - this.currentFunction.returnValueAlloca, - "rv" - ) - ); - } - }); - } - - let switch_stmt = ir.createSwitch( - cleanup_reason, - merge_block, - scope.destinations.length + 1 - ); - if (this.currentFunction.returnValueAlloca) - switch_stmt.addCase(consts.int32(ExitableScope.REASON_RETURN), return_tramp); - - let falloff_tramp = new llvm.BasicBlock("falloff_tramp", insertFunc); - this.doInsideBBlock(falloff_tramp, () => { - ir.createBr(merge_block); - }); - switch_stmt.addCase( - consts.int32(TryExitableScope.REASON_FALLOFF_TRY), - falloff_tramp - ); - - for (let s = 0, e = scope.destinations.length; s < e; s++) { - let dest_tramp = new llvm.BasicBlock("dest_tramp", insertFunc); - var dest = scope.destinations[s]; - this.doInsideBBlock(dest_tramp, () => { - if (dest.reason == TryExitableScope.REASON_BREAK) dest.scope.exitAft(true); - else if (dest.reason == TryExitableScope.REASON_CONTINUE) - dest.scope.exitFore(); - }); - switch_stmt.addCase(dest.id, dest_tramp); - } - }); - } - - ir.setInsertPoint(merge_block); - } - - handleTemplateDefaultHandlerCall(exp) { - // we should probably only inline the construction of the string if substitutions.length < $some-number - let cooked_strings = exp.arguments[0].elements; - let substitutions = exp.arguments[1].elements; - - let cooked_i = 0; - let sub_i = 0; - let strval = null; - - let concat_string = (s) => { - if (!strval) strval = s; - else strval = this.createCall(this.ejs_runtime.string_concat, [strval, s], "strconcat"); - }; - - while (cooked_i < cooked_strings.length) { - let c = cooked_strings[cooked_i]; - cooked_i += 1; - if (c.length !== 0) concat_string(this.getAtom(c.value)); - if (sub_i < substitutions.length) { - let sub = this.visit(substitutions[sub_i]); - concat_string(this.createCall(this.ejs_runtime.ToString, [sub], "subToString")); - sub_i += 1; - } - } - - return strval; - } - - handleTemplateCallsite(exp) { - // we expect to be called with context something of the form: - // - // function generate_callsiteId0 () { - // %templateCallsite(%callsiteId_0, - // [], // raw - // [] // cooked - // }); - // } - // - // and we need to generate something along the lines of: - // - // global const %callsiteId_0 = null; // an llvm IR construct - // - // function generate_callsiteId0 () { - // if (!%callsiteId_0) { - // _ejs_gc_add_root(&%callsiteId_0); - // %callsiteId_0 = []; // cooked - // %callsiteId_0.raw = []; - // %callsiteId_0.freeze(); - // } - // return callsiteId_0; - // } - // - // our containing function already exists, so we just - // need to replace the intrinsic with the new contents. - // - // XXX there's no reason to dynamically create the - // callsite, other than it being easier for now. The - // callsite id's structure is known at compile time so - // everything could be allocated from the data segment - // and just used from there (much the same way we do - // with string literals.) - - let callsite_id = exp.arguments[0].value; - let callsite_raw_literal = exp.arguments[1]; - let callsite_cooked_literal = exp.arguments[2]; - - let insertBlock = ir.getInsertBlock(); - let insertFunc = insertBlock.parent; - - let then_bb = new llvm.BasicBlock("then", insertFunc); - let merge_bb = new llvm.BasicBlock("merge", insertFunc); - - let callsite_alloca = this.createAlloca( - this.currentFunction, - types.EjsValue, - `local_${callsite_id}` - ); - - let callsite_global = new llvm.GlobalVariable( - this.module, - types.EjsValue, - callsite_id, - llvm.Constant.getAggregateZero(types.EjsValue), - false - ); - let global_callsite_load = this.createEjsValueLoad(callsite_global, "load_global_callsite"); - ir.createStore(global_callsite_load, callsite_alloca); - - let callsite_load = ir.createLoad(types.EjsValue, callsite_alloca, "load_local_callsite"); - - let isnull = this.isNumber(callsite_load); - ir.createCondBr(isnull, then_bb, merge_bb); - - this.doInsideBBlock(then_bb, () => { - this.createCall(this.ejs_runtime.gc_add_root, [callsite_global], ""); - // XXX missing: register callsite_obj gc root - let callsite_cooked = this.visit(callsite_cooked_literal); - let callsite_raw = this.visit(callsite_raw_literal); - - let frozen_raw = this.createCall( - this.ejs_runtime.object_freeze, - [callsite_raw], - "frozen_raw" - ); - - this.createCall( - this.ejs_runtime.object_setprop, - [callsite_cooked, this.visit(b.literal("raw")), frozen_raw], - "propstore_raw" - ); - - let frozen_cooked = this.createCall( - this.ejs_runtime.object_freeze, - [callsite_cooked], - "frozen_cooked" - ); - ir.createStore(frozen_cooked, callsite_global); - ir.createStore(frozen_cooked, callsite_alloca); - ir.createBr(merge_bb); - }); - - ir.setInsertPoint(merge_bb); - return this.createRet( - ir.createLoad(types.EjsValue, callsite_alloca, "load_local_callsite") - ); - } - - handleModuleGet(exp) { - let moduleString = this.visit(exp.arguments[0].value); - return this.createCall(this.ejs_runtime.module_get, [moduleString], "moduletmp"); - } - - handleModuleSlotRef(exp, opencode) { - let moduleString = exp.arguments[0].value; - let exportId = exp.arguments[1].value; - let module_global; - - if (moduleString.endsWith(".js")) - moduleString = moduleString.substring(0, moduleString.length - 3); - if (moduleString === this.this_module_info.path) { - module_global = this.this_module_global; - } else { - module_global = this.import_module_globals.get(moduleString); - } - module_global = ir.createPointerCast(module_global, types.EjsModule.pointerTo(), ""); - - let slotnum = this.allModules.get(moduleString).exports.get(exportId).slot_num; - - if (opencode && this.triple.pointerSize() === 64) { - return ir.createInBoundsGetElementPointer( - types.EjsModule, - module_global, - [consts.int64(0), consts.int32(3), consts.int64(slotnum)], - "slot_ref" - ); - } - - return this.createCall( - this.ejs_runtime.module_get_slot_ref, - [module_global, consts.int32(slotnum)], - "module_slot" - ); - } - - handleModuleGetSlot(exp, opencode) { - let slot_ref = this.handleModuleSlotRef(exp, opencode); - return ir.createLoad(types.EjsValue, slot_ref, "module_slot_load"); - } - - handleModuleSetSlot(exp, opencode) { - let arg = exp.arguments[2]; - - let slot_ref = this.handleModuleSlotRef(exp, opencode); - this.storeToDest(slot_ref, arg); - - return ir.createLoad(types.EjsValue, slot_ref, "load_slot"); // do we need this? we don't need to keep assignment expression semantics for this - } - - handleModuleGetExotic(exp) { - let moduleString = exp.arguments[0].value; - - if (this.opencode_intrinsics.moduleGetExotic) { - if (moduleString === this.this_module_info.path) { - let module_global = this.this_module_global; - return this.emitEjsvalFromPtr(module_global, "exotic"); - } else if (this.import_module_globals.has(moduleString)) { - let module_global = this.import_module_globals.get(moduleString); - return this.emitEjsvalFromPtr(module_global, "exotic"); - } - } - - // fallback for the opencoded version as well as the non-opencoded - // version. - return this.createCall( - this.ejs_runtime.module_get, - [this.visit(exp.arguments[0])], - "get_module_exotic" - ); - } - - handleGetNewTarget() { - return this.createEjsValueLoad(this.findIdentifierInScope("%newTarget"), "new_target_load"); - } - - handleGetArgumentsObject() { - let arguments_alloca = this.createAlloca( - this.currentFunction, - types.EjsValue, - "local_arguments_object" - ); - let saved_insert_point = ir.getInsertBlock(); - ir.setInsertPoint(this.currentFunction.entry_bb); - - let load_argc = this.createLoad( - types.Int32, - this.currentFunction.topScope.get("%argc"), - "argc_load" - ); - let load_args = this.createLoad( - types.EjsValue.pointerTo(), - this.currentFunction.topScope.get("%args"), - "args_load" - ); - - let args_new = this.ejs_runtime.arguments_new; - let arguments_object = this.createCall( - args_new, - [load_argc, load_args], - "argstmp", - !args_new.doesNotThrow - ); - ir.createStore(arguments_object, arguments_alloca); - this.currentFunction.topScope.set("arguments", arguments_alloca); - - ir.setInsertPoint(saved_insert_point); - return this.createEjsValueLoad(arguments_alloca, "load_arguments"); - } - - handleGetLocal(exp) { - return this.createEjsValueLoad( - this.findIdentifierInScope(exp.arguments[0].name), - `load_${exp.arguments[0].name}` - ); - } - handleGetGlobal(exp) { - return this.loadGlobal(exp.arguments[0]); - } - - handleSetLocal(exp) { - let dest = this.findIdentifierInScope(exp.arguments[0].name); - if (!dest) throw new Error(`identifier not found: ${exp.arguments[0].name}`); - let arg = exp.arguments[1]; - this.storeToDest(dest, arg); - return ir.createLoad(types.EjsValue, dest, "load_val"); - } - - handleSetGlobal(exp) { - let gname = exp.arguments[0].name; - - if (this.options.frozen_global) - throw new SyntaxError( - `cannot set global property '${exp.arguments[0].name}' when using --frozen-global` - ); - - let gatom = this.getAtom(gname); - let value = this.visit(exp.arguments[1]); - - return this.createCall( - this.ejs_runtime.global_setprop, - [gatom, value], - `globalpropstore_${gname}` - ); - } - - // this method assumes it's called in an opencoded context - emitEjsvalTo(val, type, prefix) { - if (this.triple.pointerSize() === 64) { - let payload = this.createEjsvalAnd( - val, - consts.int64_lowhi(0x7fff, 0xffffffff), - `${prefix}_payload` - ); - return ir.createIntToPtr(payload, type, `${prefix}_load`); - } else { - throw new Error("emitEjsvalTo not implemented for this case"); - } - } - - emitEjsvalFromPtr(ptr, prefix) { - if (this.triple.pointerSize() === 64) { - let fromptr_alloca = this.createAlloca( - this.currentFunction, - types.EjsValue, - `${prefix}_ejsval` - ); - let intval = ir.createPtrToInt(ptr, types.Int64, `${prefix}_intval`); - let payload = ir.createOr( - intval, - consts.int64_lowhi(0xfffc0000, 0x00000000), - `${prefix}_payload` - ); - let alloca_as_int64 = ir.createBitCast( - fromptr_alloca, - types.Int64.pointerTo(), - `${prefix}_alloca_asptr` - ); - ir.createStore(payload, alloca_as_int64, `${prefix}_store`); - return ir.createLoad(types.EjsValue, fromptr_alloca, `${prefix}_load`); - } else { - throw new Error("emitEjsvalTo not implemented for this case"); - } - } - - emitEjsvalToObjectPtr(val) { - return this.emitEjsvalTo(val, types.EjsObject.pointerTo(), "to_objectptr"); - } - - emitEjsvalToClosureEnvPtr(val) { - return this.emitEjsvalTo(val, types.EjsClosureEnv.pointerTo(), "to_ptr"); - } - - // this method assumes it's called in an opencoded context - emitLoadSpecops(obj) { - if (this.triple.pointerSize() === 64) { - // %1 = getelementptr inbounds %struct._EJSObject* %obj, i64 0, i32 1 - // %specops_load = load %struct.EJSSpecOps** %1, align 8, !tbaa !0 - let specops_slot = ir.createInBoundsGetElementPointer( - types.EjsObject, - obj, - [consts.int64(0), consts.int32(1)], - "specops_slot" - ); - return ir.createLoad(types.EjsValue, specops_slot, "specops_load"); - } else { - throw new Error("emitLoadSpecops not implemented for this case"); - } - } - - emitThrowNativeError(errorCode, errorMessage) { - this.createCall( - this.ejs_runtime.throw_nativeerror_utf8, - [consts.int32(errorCode), consts.string(ir, errorMessage)], - "", - true - ); - return ir.createUnreachable(); - } - - // this method assumes it's called in an opencoded context - emitLoadEjsFunctionClosureFunc(closure) { - if (this.triple.pointerSize() === 64) { - let func_slot_gep = ir.createInBoundsGetElementPointer( - types.EjsClosureEnv.pointerTo(), - closure, - [consts.int64(1)], - "func_slot_gep" - ); - let func_slot = ir.createBitCast( - func_slot_gep, - this.abi - .createFunctionType(types.EjsValue, [ - types.EjsValue, - types.EjsValue, - types.Int32, - types.EjsValue.pointerTo(), - ]) - .pointerTo() - .pointerTo(), - "func_slot" - ); - return ir.createLoad(types.EjsValue, func_slot, "func_load"); - } else { - throw new Error("emitLoadEjsFunctionClosureFunc not implemented for this case"); - } - } - - // this method assumes it's called in an opencoded context - emitLoadEjsFunctionClosureEnv(closure) { - if (this.triple.pointerSize() === 64) { - let env_slot_gep = ir.createInBoundsGetElementPointer( - types.EjsClosureEnv.pointerTo(), - closure, - [consts.int64(1), consts.int32(1)], - "env_slot_gep" - ); - let env_slot = ir.createBitCast(env_slot_gep, types.EjsValue.pointerTo(), "env_slot"); - return ir.createLoad(types.EjsValue, env_slot, "env_load"); - } else { - throw new Error("emitLoadEjsFunctionClosureEnv not implemented for this case"); - } - } - - handleInvokeClosure(exp, opencode) { - let insertBlock = ir.getInsertBlock(); - let insertFunc = insertBlock.parent; - - if (!this.currentFunction.scratch_area) { - throw new Error( - `Internal error: function has no scratch space and makes a [[Call]] call with ${exp.arguments.length} arguments` - ); - } - - let argv = this.visitArgsForCall(this.ejs_runtime.invoke_closure, true, exp.arguments); - - if (opencode && this.triple.pointerSize() === 64) { - // - // generate basically the following code: - // - // f = argv[0] - // if (EJSVAL_IS_FUNCTION(F) - // f->func(f->env, argv[1], argv[2], argv[3]) - // else - // _ejs_invoke_closure(...argv) - // - let candidate_is_object_bb = new llvm.BasicBlock("candidate_is_object_bb", insertFunc); - var direct_invoke_bb = new llvm.BasicBlock("direct_invoke_bb", insertFunc); - var runtime_invoke_bb = new llvm.BasicBlock("runtime_invoke_bb", insertFunc); - var invoke_merge_bb = new llvm.BasicBlock("invoke_merge_bb", insertFunc); - - let cmp = this.isObject(argv[0]); - ir.createCondBr(cmp, candidate_is_object_bb, runtime_invoke_bb); - - var call_result_alloca = this.createAlloca( - this.currentFunction, - types.EjsValue, - "call_result" - ); - - this.doInsideBBlock(candidate_is_object_bb, () => { - let closure = this.emitEjsvalToObjectPtr(argv[0]); - let cmp = this.isObjectFunction(closure); - - ir.createCondBr(cmp, direct_invoke_bb, runtime_invoke_bb); - - // in the successful case we modify our argv with the responses and directly invoke the closure func - this.doInsideBBlock(direct_invoke_bb, () => { - let func_load = this.emitLoadEjsFunctionClosureFunc(closure); - let env_load = this.emitLoadEjsFunctionClosureEnv(closure); - let direct_call_result = this.createCall( - func_load, - [env_load, argv[1], argv[2], argv[3], argv[4], argv[5]], - "callresult" - ); - ir.createStore(direct_call_result, call_result_alloca); - ir.createBr(invoke_merge_bb); - }); - - this.doInsideBBlock(runtime_invoke_bb, () => { - let runtime_call_result = this.createCall( - this.ejs_runtime.invoke_closure, - argv, - "callresult", - true - ); - ir.createStore(runtime_call_result, call_result_alloca); - ir.createBr(invoke_merge_bb); - }); - }); - - ir.setInsertPoint(invoke_merge_bb); - - return ir.createLoad(types.EjsValue, call_result_alloca, "call_result_load"); - } else { - return this.createCall(this.ejs_runtime.invoke_closure, argv, "call", true); - } - } - - handleConstructClosure(exp) { - let this_alloca = this.createAlloca(this.currentFunction, types.EjsValue, "this_alloca"); - this.storeUndefined(this_alloca, "store_undefined_this"); - - if (!this.currentFunction.scratch_area) { - throw new Error( - `Internal error: function has no scratch space and makes a [[Construct]] call with ${exp.arguments.length} arguments` - ); - } - - let argv = this.visitArgsForConstruct( - this.ejs_runtime.construct_closure, - exp.arguments, - this_alloca - ); - - return this.createCall(this.ejs_runtime.construct_closure, argv, "construct", true); - } - - handleConstructSuper(exp) { - let this_ptr = this.createLoad( - types.EjsValue.pointerTo(), - this.findIdentifierInScope("%this"), - "this_ptr" - ); - let newTarget = this.createEjsValueLoad( - this.findIdentifierInScope("%newTarget"), - "load_newTarget" - ); - - let argv = this.visitArgsForConstruct( - this.ejs_runtime.construct_closure, - exp.arguments, - this_ptr, - newTarget - ); - - return this.createCall(this.ejs_runtime.construct_closure, argv, "construct_super", true); - } - - handleConstructSuperApply(exp) { - let this_ptr = this.createLoad( - types.EjsValue.pointerTo(), - this.findIdentifierInScope("%this"), - "this_ptr" - ); - let newTarget = this.createEjsValueLoad( - this.findIdentifierInScope("%newTarget"), - "load_newTarget" - ); - - let argv = this.visitArgsForConstruct( - this.ejs_runtime.construct_closure_apply, - exp.arguments, - this_ptr, - newTarget - ); - - return this.createCall( - this.ejs_runtime.construct_closure_apply, - argv, - "construct_super_apply", - true - ); - } - - handleSetConstructorKindDerived(exp) { - let ctor = this.visit(exp.arguments[0]); - return this.createCall(this.ejs_runtime.set_constructor_kind_derived, [ctor], ""); - } - - handleSetConstructorKindBase(exp) { - let ctor = this.visit(exp.arguments[0]); - return this.createCall(this.ejs_runtime.set_constructor_kind_base, [ctor], ""); - } - - handleMakeGenerator(exp) { - let argv = this.visitArgsForCall(this.ejs_runtime.make_generator, false, exp.arguments); - return this.createCall(this.ejs_runtime.make_generator, argv, "generator"); - } - - handleGeneratorYield(exp) { - let argv = this.visitArgsForCall(this.ejs_runtime.generator_yield, false, exp.arguments); - return this.createCall(this.ejs_runtime.generator_yield, argv, "yield"); - } - - handleMakeClosure(exp) { - let argv = this.visitArgsForCall(this.ejs_runtime.make_closure, false, exp.arguments); - return this.createCall(this.ejs_runtime.make_closure, argv, "closure_tmp"); - } - - handleMakeClosureNoEnv(exp) { - let argv = this.visitArgsForCall(this.ejs_runtime.make_closure_noenv, false, exp.arguments); - return this.createCall(this.ejs_runtime.make_closure_noenv, argv, "closure_tmp"); - } - - handleMakeAnonClosure(exp) { - let argv = this.visitArgsForCall(this.ejs_runtime.make_anon_closure, false, exp.arguments); - return this.createCall(this.ejs_runtime.make_anon_closure, argv, "closure_tmp"); - } - - handleCreateArgScratchArea(exp) { - let argsArrayType = llvm.ArrayType.get(types.EjsValue, exp.arguments[0].value); - this.currentFunction.scratch_length = exp.arguments[0].value; - this.currentFunction.scratch_area = this.createAlloca( - this.currentFunction, - argsArrayType, - "args_scratch_area" - ); - this.currentFunction.scratch_area.setAlignment(8); - return this.currentFunction.scratch_area; - } - - handleMakeClosureEnv(exp) { - let size = exp.arguments[0].value; - return this.createCall(this.ejs_runtime.make_closure_env, [consts.int32(size)], "env_tmp"); - } - - handleGetSlot(exp, opencode) { - // - // %ref = handleSlotRef - // %ret = load %EjsValueType* %ref, align 8 - // - let slot_ref = this.handleSlotRef(exp, opencode); - return ir.createLoad(types.EjsValue, slot_ref, "slot_ref_load"); - } - - handleSetSlot(exp, opencode) { - let new_slot_val; - - if (exp.arguments.length === 4) new_slot_val = exp.arguments[3]; - else new_slot_val = exp.arguments[2]; - - let slotref = this.handleSlotRef(exp, opencode); - - this.storeToDest(slotref, new_slot_val); - - return ir.createLoad(types.EjsValue, slotref, "load_slot"); - } - - handleSlotRef(exp, opencode) { - let env = this.visitOrNull(exp.arguments[0]); - let slotnum = exp.arguments[1].value; - - if (opencode && this.triple.pointerSize() === 64) { - let envp = this.emitEjsvalToClosureEnvPtr(env); - return ir.createInBoundsGetElementPointer( - types.EjsClosureEnv, - envp, - [consts.int64(0), consts.int32(2), consts.int64(slotnum)], - "slot_ref" - ); - } else { - return this.createCall( - this.ejs_runtime.get_env_slot_ref, - [env, consts.int32(slotnum)], - "slot_ref_tmp", - false - ); - } - } - - createEjsBoolSelect(val, falseval = false) { - let rv = ir.createSelect( - val, - this.loadBoolEjsValue(!falseval), - this.loadBoolEjsValue(falseval), - "sel" - ); - rv._ejs_returns_ejsval_bool = true; - return rv; - } - - getEjsvalBits(arg) { - let bits_alloca; - - if (this.currentFunction.bits_alloca) bits_alloca = this.currentFunction.bits_alloca; - else bits_alloca = this.createAlloca(this.currentFunction, types.EjsValue, "bits_alloca"); - - ir.createStore(arg, bits_alloca); - let bits_ptr = ir.createBitCast(bits_alloca, types.Int64.pointerTo(), "bits_ptr"); - if (!this.currentFunction.bits_alloca) this.currentFunction.bits_alloca = bits_alloca; - return ir.createLoad(types.Int64, bits_ptr, "bits_load"); - } - - createEjsvalICmpUGt(arg, i64_const, name) { - return ir.createICmpUGt(this.getEjsvalBits(arg), i64_const, name); - } - createEjsvalICmpULt(arg, i64_const, name) { - return ir.createICmpULt(this.getEjsvalBits(arg), i64_const, name); - } - createEjsvalICmpEq(arg, i64_const, name) { - return ir.createICmpEq(this.getEjsvalBits(arg), i64_const, name); - } - createEjsvalAnd(arg, i64_const, name) { - return ir.createAnd(this.getEjsvalBits(arg), i64_const, name); - } - - isObject(val) { - if (this.triple.pointerSize() === 64) { - return this.createEjsvalICmpUGt( - val, - consts.int64_lowhi(0xfffbffff, 0xffffffff), - "cmpresult" - ); - } else { - let trunc = ir.createTrunc(this.getEjsvalBits(val), types.Int32, "trunc.i"); - return ir.createICmpEq(trunc, consts.int32(-120), "cmpresult"); - } - } - - isObjectFunction(obj) { - return ir.createICmpEq( - this.emitLoadSpecops(obj), - this.ejs_runtime.function_specops, - "function_specops_cmp" - ); - } - - isObjectSymbol(obj) { - return ir.createICmpEq( - this.emitLoadSpecops(obj), - this.ejs_runtime.symbol_specops, - "symbol_specops_cmp" - ); - } - - isString(val) { - if (this.triple.pointerSize() === 64) { - let mask = this.createEjsvalAnd( - val, - consts.int64_lowhi(0xffff8000, 0x00000000), - "mask.i" - ); - return ir.createICmpEq(mask, consts.int64_lowhi(0xfffa8000, 0x00000000), "cmpresult"); - } else { - let trunc = ir.createTrunc(this.getEjsvalBits(val), types.Int32, "trunc.i"); - return ir.createICmpEq(trunc, consts.int32(-123), "cmpresult"); - } - } - - isNumber(val) { - if (this.triple.pointerSize() === 64) { - return this.createEjsvalICmpULt( - val, - consts.int64_lowhi(0xfff80001, 0x00000000), - "cmpresult" - ); - } else { - let trunc = ir.createTrunc(this.getEjsvalBits(val), types.Int32, "trunc.i"); - return ir.createICmpEq(trunc, consts.int32(-127), "cmpresult"); - } - } - - isBoolean(val) { - if (this.triple.pointerSize() === 64) { - let mask = this.createEjsvalAnd( - val, - consts.int64_lowhi(0xffff8000, 0x00000000), - "mask.i" - ); - return ir.createICmpEq(mask, consts.int64_lowhi(0xfff98000, 0x00000000), "cmpresult"); - } else { - let trunc = ir.createTrunc(this.getEjsvalBits(val), types.Int32, "trunc.i"); - return ir.createICmpEq(trunc, consts.int32(-125), "cmpresult"); - } - } - - // these two could/should be changed to check for the specific bitpattern of _ejs_true/_ejs_false - isTrue(val) { - return ir.createICmpEq( - val, - consts.ejsval_true(this.triple.pointerSize() === 32), - "cmpresult" - ); - } - isFalse(val) { - return ir.createICmpEq( - val, - consts.ejsval_false(this.triple.pointerSize() === 32), - "cmpresult" - ); - } - - isUndefined(val) { - if (this.triple.pointerSize() === 64) { - return this.createEjsvalICmpEq( - val, - consts.int64_lowhi(0xfff90000, 0x00000000), - "cmpresult" - ); - } else { - let trunc = ir.createTrunc(this.getEjsvalBits(val), types.Int32, "trunc.i"); - return ir.createICmpEq(trunc, consts.int32(-126), "cmpresult"); - } - } - - isNull(val) { - if (this.triple.pointerSize() === 64) { - return this.createEjsvalICmpEq( - val, - consts.int64_lowhi(0xfffb8000, 0x00000000), - "cmpresult" - ); - } else { - let trunc = ir.createTrunc(this.getEjsvalBits(val), types.Int32, "trunc.i"); - return ir.createICmpEq(trunc, consts.int32(-121), "cmpresult"); - } - } - - handleTypeofIsObject(exp) { - let arg = this.visitOrNull(exp.arguments[0]); - return this.createEjsBoolSelect(this.isObject(arg)); - } - - handleTypeofIsFunction(exp, opencode) { - let arg = this.visitOrNull(exp.arguments[0]); - if (opencode && this.triple.pointerSize() === 64) { - let insertBlock = ir.getInsertBlock(); - let insertFunc = insertBlock.parent; - - var typeofIsFunction_alloca = this.createAlloca( - this.currentFunction, - types.EjsValue, - "typeof_is_function" - ); - - var failure_bb = new llvm.BasicBlock("typeof_function_false", insertFunc); - let is_object_bb = new llvm.BasicBlock("typeof_function_is_object", insertFunc); - var success_bb = new llvm.BasicBlock("typeof_function_true", insertFunc); - var merge_bb = new llvm.BasicBlock("typeof_function_merge", insertFunc); - - let cmp = this.isObject(arg, true); - ir.createCondBr(cmp, is_object_bb, failure_bb); - - this.doInsideBBlock(is_object_bb, () => { - let obj = this.emitEjsvalToObjectPtr(arg); - let cmp = this.isObjectFunction(obj); - ir.createCondBr(cmp, success_bb, failure_bb); - }); - - this.doInsideBBlock(success_bb, () => { - this.storeBoolean(typeofIsFunction_alloca, true, "store_typeof"); - ir.createBr(merge_bb); - }); - - this.doInsideBBlock(failure_bb, () => { - this.storeBoolean(typeofIsFunction_alloca, false, "store_typeof"); - ir.createBr(merge_bb); - }); - - ir.setInsertPoint(merge_bb); - - let rv = ir.createLoad(types.EjsValue, typeofIsFunction_alloca, "typeof_is_function"); - rv._ejs_returns_ejsval_bool = true; - return rv; - } else { - return this.createCall( - this.ejs_runtime.typeof_is_function, - [arg], - "is_function", - false - ); - } - } - - handleTypeofIsSymbol(exp, opencode) { - let arg = this.visitOrNull(exp.arguments[0]); - if (opencode && this.triple.pointerSize() === 64) { - let insertBlock = ir.getInsertBlock(); - let insertFunc = insertBlock.parent; - - var typeofIsSymbol_alloca = this.createAlloca( - this.currentFunction, - types.EjsValue, - "typeof_is_symbol" - ); - - var failure_bb = new llvm.BasicBlock("typeof_symbol_false", insertFunc); - let is_object_bb = new llvm.BasicBlock("typeof_symbol_is_object", insertFunc); - var success_bb = new llvm.BasicBlock("typeof_symbol_true", insertFunc); - var merge_bb = new llvm.BasicBlock("typeof_symbol_merge", insertFunc); - - let cmp = this.isObject(arg, true); - ir.createCondBr(cmp, is_object_bb, failure_bb); - - this.doInsideBBlock(is_object_bb, () => { - let obj = this.emitEjsvalToObjectPtr(arg); - let cmp = this.isObjectSymbol(obj); - ir.createCondBr(cmp, success_bb, failure_bb); - }); - - this.doInsideBBlock(success_bb, () => { - this.storeBoolean(typeofIsSymbol_alloca, true, "store_typeof"); - ir.createBr(merge_bb); - }); - - this.doInsideBBlock(failure_bb, () => { - this.storeBoolean(typeofIsSymbol_alloca, false, "store_typeof"); - ir.createBr(merge_bb); - }); - - ir.setInsertPoint(merge_bb); - - let rv = ir.createLoad(types.EjsValue, typeofIsSymbol_alloca, "typeof_is_symbol"); - rv._ejs_returns_ejsval_bool = true; - return rv; - } else { - return this.createCall(this.ejs_runtime.typeof_is_symbol, [arg], "is_symbol", false); - } - } - - handleTypeofIsString(exp) { - let arg = this.visitOrNull(exp.arguments[0]); - return this.createEjsBoolSelect(this.isString(arg)); - } - - handleTypeofIsNumber(exp) { - let arg = this.visitOrNull(exp.arguments[0]); - return this.createEjsBoolSelect(this.isNumber(arg)); - } - - handleTypeofIsBoolean(exp) { - let arg = this.visitOrNull(exp.arguments[0]); - return this.createEjsBoolSelect(this.isBoolean(arg)); - } - - handleIsUndefined(exp) { - let arg = this.visitOrNull(exp.arguments[0]); - return this.createEjsBoolSelect(this.isUndefined(arg)); - } - - handleIsNull(exp) { - let arg = this.visitOrNull(exp.arguments[0]); - return this.createEjsBoolSelect(this.isNull(arg)); - } - - handleIsNullOrUndefined(exp, opencode) { - let arg = this.visitOrNull(exp.arguments[0]); - if (opencode) - return this.createEjsBoolSelect( - ir.createOr(this.isNull(arg), this.isUndefined(arg), "or") - ); - else - return this.createCall( - this.ejs_binops["=="], - [this.loadNullEjsValue(), arg], - "is_null_or_undefined", - false - ); - } - - handleBuiltinUndefined() { - return this.loadUndefinedEjsValue(); - } - - handleSetPrototypeOf(exp) { - let obj = this.visitOrNull(exp.arguments[0]); - let proto = this.visitOrNull(exp.arguments[1]); - return this.createCall( - this.ejs_runtime.object_set_prototype_of, - [obj, proto], - "set_prototype_of", - true - ); - // we should check the return value of set_prototype_of - } - - handleObjectCreate(exp) { - let proto = this.visitOrNull(exp.arguments[0]); - return this.createCall(this.ejs_runtime.object_create, [proto], "object_create", true); - // we should check the return value of object_create - } - - handleArrayFromRest(exp) { - let rest_name = exp.arguments[0].value; - let formal_params_length = exp.arguments[1].value; - - let has_rest_bb = new llvm.BasicBlock("has_rest_bb", this.currentFunction); - let no_rest_bb = new llvm.BasicBlock("no_rest_bb", this.currentFunction); - let rest_merge_bb = new llvm.BasicBlock("rest_merge", this.currentFunction); - - let rest_alloca = this.createAlloca( - this.currentFunction, - types.EjsValue, - "local_rest_object" - ); - - let load_argc = this.createLoad( - types.Int32, - this.currentFunction.topScope.get("%argc"), - "argc_load" - ); - - let cmp = ir.createICmpSGt(load_argc, consts.int32(formal_params_length), "argcmpresult"); - ir.createCondBr(cmp, has_rest_bb, no_rest_bb); - - ir.setInsertPoint(has_rest_bb); - // we have > args than are declared, shove the rest into the rest parameter - let load_args = this.createLoad( - types.EjsValue.pointerTo(), - this.currentFunction.topScope.get("%args"), - "args_load" - ); - let gep = ir.createInBoundsGetElementPointer( - types.EjsValue.pointerTo(), - load_args, - [consts.int32(formal_params_length)], - "rest_arg_gep" - ); - load_argc = ir.createNswSub(load_argc, consts.int32(formal_params_length)); - load_argc = ir.createZExt(load_argc, types.Int64); - let rest_value = this.createCall( - this.ejs_runtime.array_new_copy, - [load_argc, gep], - "argstmp", - !this.ejs_runtime.array_new_copy.doesNotThrow - ); - ir.createStore(rest_value, rest_alloca); - ir.createBr(rest_merge_bb); - - ir.setInsertPoint(no_rest_bb); - // we have <= args than are declared, so the rest parameter is just an empty array - rest_value = this.createCall( - this.ejs_runtime.array_new, - [consts.int64(0), consts.False()], - "arrtmp", - !this.ejs_runtime.array_new.doesNotThrow - ); - ir.createStore(rest_value, rest_alloca); - ir.createBr(rest_merge_bb); - - ir.setInsertPoint(rest_merge_bb); - - this.currentFunction.topScope.set(rest_name, rest_alloca); - this.currentFunction.restArgPresent = true; - - return ir.createLoad(types.EjsValue, rest_alloca, "load_rest"); - } - - handleArrayFromSpread(exp) { - let arg_count = exp.arguments.length; - let spread_alloca = this.currentFunction.scratch_area; - - let visited = []; - for (let a of exp.arguments) visited.push(this.visitOrNull(a)); - - const scratchAreaType = llvm.ArrayType.get( - types.EjsValue, - this.currentFunction.scratch_length - ); - - visited.forEach((a, i) => { - let gep = ir.createGetElementPointer( - scratchAreaType, - spread_alloca, - [consts.int32(0), consts.int64(i)], - `spread_gep_${i}` - ); - ir.createStore(visited[i], gep, `spread[${i}]-store`); - }); - - let argsCast = ir.createGetElementPointer( - scratchAreaType, - spread_alloca, - [consts.int32(0), consts.int64(0)], - "spread_call_args_load" - ); - - let argv = [consts.int32(arg_count), argsCast]; - return this.createCall(this.ejs_runtime.array_from_iterables, argv, "spread_arr"); - } - - handleGetArg(exp) { - // the intrinsic looks like this: %getArg(args_index, default_value) - // - // if (argc > args_index) { - // result = default_value - // } - // else { - // if (default_value === undefined) { - // result = default_value - // } - // else { - // if (args[args_index] === undefined) { - // result = default_value - // } - // else { - // result = args[args_index] - // } - // } - // } - // - // this expanded form is only necessary when default_value is not undefined (something we know at compile time). - // when default_value is undefined, we end up with this simpler form: - // - // if (argc > args_index) { - // result = default_value - // } - // else { - // result = args[args_index] - // } - - let arg_num = exp.arguments[0].value; - let load_argc = this.createLoad( - types.Int32, - this.currentFunction.topScope.get("%argc"), - "argc_n_load" - ); - let cmp = ir.createICmpUGE(load_argc, consts.int32(arg_num + 1), "argcmpresult"); - - let has_slot_bb = new llvm.BasicBlock(`has_${arg_num}_slot`, this.currentFunction); - let no_slot_bb = new llvm.BasicBlock(`no_${arg_num}_slot`, this.currentFunction); - let merge_bb = new llvm.BasicBlock(`arg_${arg_num}_merge`, this.currentFunction); - - let arg_value_alloca = this.createAlloca( - this.currentFunction, - types.EjsValue, - `arg_${arg_num}_value` - ); - - ir.createCondBr(cmp, has_slot_bb, no_slot_bb); - - this.doInsideBBlock(has_slot_bb, () => { - let load_args = this.createLoad( - types.EjsValue.pointerTo(), - this.currentFunction.topScope.get("%args"), - "args_load" - ); - let arg_ptr = ir.createGetElementPointer( - types.EjsValue, - load_args, - [consts.int32(arg_num)], - `arg${arg_num}_ptr` - ); - let arg_load = this.createEjsValueLoad(arg_ptr, `arg${arg_num}`); - if ( - exp.arguments.length > 1 && - (exp.arguments[1].type !== b.Literal || exp.arguments[1].value !== undefined) - ) { - // more complicated form, we need to check if the passed arg was undefined - let arg_is_undefined = this.isUndefined(arg_load); - let arg_select = ir.createSelect( - arg_is_undefined, - this.visit(exp.arguments[1]), - arg_load, - "arg_select" - ); - ir.createStore(arg_select, arg_value_alloca, "store_arg_value"); - } else { - // simplified case above, just store it into our alloca - ir.createStore(arg_load, arg_value_alloca, "store_arg_value"); - } - ir.createBr(merge_bb); - }); - - this.doInsideBBlock(no_slot_bb, () => { - // we didn't have the slot - let default_arg = this.visit(exp.arguments[1]); - ir.createStore(default_arg, arg_value_alloca, "store_arg_value"); - ir.createBr(merge_bb); - }); - - ir.setInsertPoint(merge_bb); - return ir.createLoad(types.EjsValue, arg_value_alloca, "load_arg_value"); - } - - handleCreateIterResult(exp) { - let value = this.visit(exp.arguments[0]); - let done = this.visit(exp.arguments[1]); - return this.createCall(this.ejs_runtime.create_iter_result, [value, done], "iter_result"); - } - - handleCreateIteratorWrapper(exp) { - let iter = this.visit(exp.arguments[0]); - return this.createCall(this.ejs_runtime.iterator_wrapper_new, [iter], "iter_wrapper"); - } -} - -class AddFunctionsVisitor extends TreeVisitor { - constructor(module, abi, dibuilder, difile) { - super(); - this.module = module; - this.abi = abi; - this.dibuilder = dibuilder; - this.difile = difile; - } - - visitFunction(n) { - if (n && n.id && n.id.name) n.ir_name = n.id.name; - else n.ir_name = "_ejs_anonymous"; - - // at this point point n.params includes %env as its first param, and is followed by all the formal parameters from the original - // script source. we remove the %env parameter and save off he rest of the formal parameter names, and replace the list with - // our runtime parameters. - - // remove %env from the formal parameter list, but save its name first - let env_name = n.params[0].name; - n.params.splice(0, 1); - // and store the JS formal parameters someplace else - n.formal_params = n.params; - - n.params = []; - for (let param of this.abi.ejs_params) - n.params.push({ - type: b.Identifier, - name: param.name, - llvm_type: param.llvm_type, - }); - n.params[this.abi.env_param_index].name = env_name; - - // create the llvm IR function using our platform calling convention - n.ir_func = types.takes_builtins( - this.abi.createFunction( - this.module, - n.ir_name, - this.abi.ejs_return_type, - n.params.map((param) => param.llvm_type) - ) - ); - if (!n.toplevel) n.ir_func.setInternalLinkage(); - - let lineno = 0; - if (n.loc) { - lineno = n.loc.start.line; - } - if (this.dibuilder && this.difile) - n.ir_func.debug_info = this.dibuilder.createFunction( - this.difile, - n.ir_name, - n.displayName || n.ir_name, - this.difile, - lineno, - false, - true, - lineno, - 0, - true, - n.ir_func - ); - - let ir_args = n.ir_func.args; - n.params.forEach((param, i) => { - ir_args[i].setName(param.name); - }); - - // we don't need to recurse here since we won't have nested functions at this point - return n; - } -} - -function insert_toplevel_func(tree, moduleInfo) { - let toplevel = { - type: b.FunctionDeclaration, - id: b.identifier(moduleInfo.toplevel_function_name), - displayName: "toplevel", - params: [], - defaults: [], - body: { - type: b.BlockStatement, - body: tree.body, - loc: { - start: { - line: 0, - column: 0, - }, - }, - }, - toplevel: true, - loc: { - start: { - line: 0, - column: 0, - }, - }, - }; - - tree.body = [toplevel]; - return tree; -} - -export function compile(tree, base_output_filename, source_filename, module_infos, options, triple) { - let abi = triple.abi(); - - types.initTypes(triple.pointerSize()); - - let module_filename = source_filename; - - if (module_filename.endsWith(".js")) { - module_filename = module_filename.substring(0, module_filename.length - 3); - } - - let this_module_info = module_infos.get(module_filename); - - tree = insert_toplevel_func(tree, this_module_info); - - debug.log(() => escodegenerate(tree)); - - let toplevel_name = tree.body[0].id.name; - - //debug.log 1, 'before closure conversion' - //debug.log 1, -> escodegenerate tree - - tree = closure_convert(tree, source_filename, module_infos, options); - - debug.log(1, "after closure conversion"); - // debug.log(1, () => escodegenerate(tree)); - - /* - tree = typeinfer.run tree - - debug.log 1, 'after type inference' - debug.log 1, -> escodegenerate tree - */ - tree = optimizations.run(tree); - - debug.log(1, "after optimization"); - // debug.log(1, () => escodegenerate(tree)); - - let module = new llvm.Module(base_output_filename); - - module.toplevel_name = toplevel_name; - - let module_accessors = []; - this_module_info.exports.forEach((export_info, key) => { - let module_prop = undefined; - let f = this_module_info.getExportGetter(key); - if (f) { - if (!module_prop) module_prop = { key }; - module_prop.getter = f; - tree.body.push(f); - } - f = this_module_info.getExportSetter(key); - if (f) { - if (!module_prop) module_prop = { key }; - module_prop.setter = f; - tree.body.push(f); - } - if (module_prop) module_accessors.push(module_prop); - }); - - let dibuilder; - let difile; - - if (options.debug) { - dibuilder = new llvm.DIBuilder(module); - difile = dibuilder.createFile(source_filename + ".js", process.cwd()); - - dibuilder.createCompileUnit(source_filename + ".js", process.cwd(), "ejs", true, "", 2); - } - - let visitor = new AddFunctionsVisitor(module, abi, dibuilder, difile); - - tree = visitor.visit(tree); - - // debug.log(() => escodegenerate(tree)); - - visitor = new LLVMIRVisitor( - module, - source_filename, - triple, - options, - abi, - module_infos, - this_module_info, - dibuilder, - difile - ); - - if (options.debug) dibuilder.finalize(); - - visitor.emitModuleInfo(); - - visitor.visit(tree); - - visitor.emitModuleResolution(module_accessors); - - return module; -} diff --git a/lib/compiler.ts b/lib/compiler.ts new file mode 100644 index 00000000..74f9921b --- /dev/null +++ b/lib/compiler.ts @@ -0,0 +1,1211 @@ +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: + */ + +import * as llvm from "@llvm"; + +import { preEIRConvert as pre_eir_convert } from "./desugar"; +import * as types from "./types"; +import * as consts from "./consts"; +import * as runtime from "./runtime"; +import * as debug from "./debug"; + +import * as b from "./ast-builder"; +import { startGenerator } from "./echo-util"; + +import { ABI } from "./abi"; +import { SRetABI } from "./sret-abi"; +import { collectEIRToplevel } from "./eir/integrate"; +import type { ModuleAccessor } from "./eir/integrate"; +import { EIREmitter, VisitorSurface } from "./eir/emit"; +import { runTypeAnalysisProbe } from "./eir/oracle"; +import type * as e from "./estree"; +import type { CompilerOptions } from "./options"; +import type { ModuleInfo, JSModuleInfo } from "./module-info"; +import type { Triple } from "./triple"; +import type { RuntimeInterface } from "./runtime"; + +const ir = llvm.IRBuilder; + +const hasOwn = Object.prototype.hasOwnProperty; + +// the state emitModuleInfo/emitModuleResolution thread between them +class LLVMIRVisitor implements VisitorSurface { + module: llvm.Module; + filename: string; + triple: Triple; + options: CompilerOptions; + abi: ABI; + allModules: Map; + this_module_info: JSModuleInfo; + dibuilder: llvm.DIBuilder | undefined; + difile: llvm.DIFile | undefined; + idgen: () => number; + genRecordId?: () => number; + llvm_intrinsics: { gcroot: () => llvm.EjsFunction }; + ejs_runtime: RuntimeInterface; + ejs_binops: Record; + ejs_atoms: Record; + ejs_globals: Record; + ejs_symbols: Record; + module_atoms: Map; + // the module's interned guard shapes (imms.shape key + // -> the i32 shape-index global + its ordered fields), filled by the + // EIR emitter's has_shape lowering and flushed into the literal-init + // function by emitShapeInterns (the atom-table precedent) + module_shapes: Map< + string, + { global: llvm.GlobalVariable; fields: { name: string; repr: string }[] } + >; + literalInitializationFunction: llvm.EjsFunction; + literalInitializationDebugInfo: llvm.DISubprogram | undefined; + literalInitializationBB: llvm.BasicBlock; + currentFunction: llvm.EjsFunction | null = null; + // module scaffolding state (set by emitModuleInfo / emitEIRToplevel) + this_module_global!: llvm.GlobalVariable; + this_module_type!: llvm.StructType; + this_module_initted!: llvm.GlobalVariable; + import_module_globals!: Map; + resolve_modules_bb!: llvm.BasicBlock; + toplevel_body_bb!: llvm.BasicBlock; + toplevel_function!: llvm.EjsFunction; + eir_toplevel_entry_bb: llvm.BasicBlock | null = null; + eir_emitter?: EIREmitter; + eir_emitted?: Map>; + eir_toplevel_fns!: Map; + // the shape-intern init function (null when the + // module guards no shapes), built by emitShapeInterns and called by + // emitModuleResolution after literal initialization + shape_init_function: llvm.EjsFunction | null = null; + + constructor( + module: llvm.Module, + filename: string, + triple: Triple, + options: CompilerOptions, + abi: ABI, + allModules: Map, + this_module_info: JSModuleInfo, + dibuilder: llvm.DIBuilder | undefined, + difile: llvm.DIFile | undefined + ) { + this.module = module; + this.filename = filename; + this.triple = triple; + this.options = options; + this.abi = abi; + this.allModules = allModules; + this.this_module_info = this_module_info; + this.dibuilder = dibuilder; + this.difile = difile; + + this.idgen = startGenerator(); + + if (this.options.record_types) this.genRecordId = startGenerator(); + + this.llvm_intrinsics = { + gcroot: () => module.getOrInsertIntrinsic("@llvm.gcroot"), + }; + + this.ejs_runtime = runtime.createInterface(module, this.abi); + this.ejs_binops = runtime.createBinopsInterface(module, this.abi); + this.ejs_atoms = runtime.createAtomsInterface(module); + this.ejs_globals = runtime.createGlobalsInterface(module); + this.ejs_symbols = runtime.createSymbolsInterface(module); + + this.module_atoms = new Map(); + this.module_shapes = new Map(); + + const init_function_name = `_ejs_module_init_string_literals_${this.filename}`; + this.literalInitializationFunction = this.module.getOrInsertFunction( + init_function_name, + types.Void, + [] + ); + + if (this.options.debug) + this.literalInitializationDebugInfo = this.dibuilder!.createFunction( + this.difile!, + init_function_name, + init_function_name, + this.difile!, + 0, + false, + true, + 0, + 0, + true, + this.literalInitializationFunction + ); + + // this function is only ever called by this module's toplevel + this.literalInitializationFunction.setInternalLinkage(); + + + let entry_bb = new llvm.BasicBlock("entry", this.literalInitializationFunction); + let return_bb = new llvm.BasicBlock("return", this.literalInitializationFunction); + + if (this.options.debug) + ir.setCurrentDebugLocation( + llvm.DebugLoc.get(0, 0, this.literalInitializationDebugInfo!) + ); + + this.doInsideBBlock(entry_bb, () => { + ir.createBr(return_bb); + }); + this.doInsideBBlock(return_bb, () => { + //this.createCall this.ejs_runtime.log, [consts.string(ir, 'done with literal initialization')], '' + ir.createRetVoid(); + }); + + this.literalInitializationBB = entry_bb; + } + + // lots of helper methods + + emitModuleInfo(): void { + this.this_module_type = types.getModuleSpecificType( + this.this_module_info.module_name, + this.this_module_info.slot_num + ); + + this.this_module_global = new llvm.GlobalVariable( + this.module, + this.this_module_type, + this.this_module_info.module_name, + llvm.Constant.getAggregateZero(this.this_module_type), + true + ); + this.import_module_globals = new Map(); + for (let import_module_string of this.this_module_info.importList) { + const import_module_info = this.allModules.get(import_module_string)!; + if (!import_module_info.isNative()) + this.import_module_globals.set( + import_module_string, + new llvm.GlobalVariable( + this.module, + types.EjsModule, + import_module_info.module_name, + null, + true + ) + ); + } + this.this_module_initted = new llvm.GlobalVariable( + this.module, + types.Bool, + `${this.this_module_info.module_name}_initialized`, + consts.False(), + false + ); + } + + emitModuleResolution(module_accessors: ModuleAccessor[]): llvm.Value { + // this.loadUndefinedEjsValue depends on this + this.currentFunction = this.toplevel_function; + + ir.setInsertPoint(this.resolve_modules_bb); + if (this.options.debug) + ir.setCurrentDebugLocation(llvm.DebugLoc.get(0, 0, this.currentFunction!.debug_info!)); + + let uninitialized_bb = new llvm.BasicBlock("module_uninitialized", this.toplevel_function); + let initialized_bb = new llvm.BasicBlock("module_initialized", this.toplevel_function); + + let load_init_flag = ir.createLoad(types.Bool, this.this_module_initted, "load_init_flag"); + let load_init_cmp = ir.createICmpEq(load_init_flag, consts.False(), "load_init_cmp"); + + ir.createCondBr(load_init_cmp, uninitialized_bb, initialized_bb); + + ir.setInsertPoint(uninitialized_bb); + ir.createStore(consts.True(), this.this_module_initted); + + ir.createCall( + this.literalInitializationFunction.type, + this.literalInitializationFunction, + [], + "" + ); + + // intern this module's guard shapes right after + // the atoms they name are initialized + if (this.shape_init_function) + ir.createCall(this.shape_init_function.type, this.shape_init_function, [], ""); + + // fill in the information we know about this module + // our name + let name_slot = ir.createInBoundsGetElementPointer( + this.this_module_type, + this.this_module_global, + [consts.int32(0), consts.int32(1)], + "name_slot" + ); + ir.createStore(consts.string(ir, this.this_module_info.path), name_slot); + + // num_exports + let num_exports_slot = ir.createInBoundsGetElementPointer( + this.this_module_type, + this.this_module_global, + [consts.int32(0), consts.int32(2)], + "num_exports_slot" + ); + ir.createStore(consts.int32(this.this_module_info.slot_num), num_exports_slot); + + // define our accessor properties. getter/setter are EIR function + // names, resolved against the toplevel module's emitted functions + for (let accessor of module_accessors) { + let get_func = + (accessor.getter && this.eir_toplevel_fns.get(accessor.getter)) || + consts.Null(types.EjsClosureFunc); + let set_func = + (accessor.setter && this.eir_toplevel_fns.get(accessor.setter)) || + consts.Null(types.EjsClosureFunc); + let module_arg = ir.createPointerCast( + this.this_module_global, + types.EjsModule.pointerTo(), + "" + ); + ir.createCall( + this.ejs_runtime.module_add_export_accessors.type, + this.ejs_runtime.module_add_export_accessors, + [module_arg, consts.string(ir, accessor.key), get_func, set_func], + "" + ); + } + + for (let import_module_string of this.this_module_info.importList) { + let import_module = this.import_module_globals.get(import_module_string); + if (import_module) { + this.createCall(this.ejs_runtime.module_resolve, [import_module], ""); + } + } + + ir.createBr(this.toplevel_body_bb); + + ir.setInsertPoint(initialized_bb); + let rv = this.createRet(this.loadUndefinedEjsValue()); + + // an EIR-owned toplevel defers its entry branch to here: all the + // cached-literal initializers this function will ever append to + // entry_bb have been appended by now + if (this.eir_toplevel_entry_bb) { + ir.setInsertPoint(this.eir_toplevel_entry_bb); + ir.createBr(this.resolve_modules_bb); + this.eir_toplevel_entry_bb = null; + } + return rv; + } + + // result should be the landingpad's value + doInsideBBlock(bb: llvm.BasicBlock, f: () => void): void { + const saved = ir.getInsertBlock(); + ir.setInsertPoint(bb); + f(); + ir.setInsertPoint(saved); + } + + createEjsValueLoad(value: llvm.Value, name: string): llvm.Value { + const rv = ir.createLoad(types.EjsValue, value, name) as llvm.AllocaInst; + rv.setAlignment(8); + return rv; + } + + loadCachedEjsValue(name: string, init: (alloca: llvm.AllocaInst) => void): llvm.Value { + let alloca_name = `${name}_alloca`; + let load_name = `${name}_load`; + + // per-function alloca cache, dynamic-keyed on the llvm function + // (matching the historical direct-property scheme) + const fn = this.currentFunction!; + const cache = fn as unknown as Record; + let alloca = cache[alloca_name]; + if (!alloca) { + const fresh = this.createAlloca(fn, types.EjsValue, alloca_name); + cache[alloca_name] = fresh; + this.doInsideBBlock(fn.entry_bb!, () => init(fresh)); + alloca = fresh; + } + + return ir.createLoad(types.EjsValue, alloca, load_name); + } + + loadBoolEjsValue(n: boolean): llvm.Value { + const rv = this.loadCachedEjsValue(String(n), (alloca) => { + let alloca_as_int64 = ir.createBitCast( + alloca, + types.Int64.pointerTo(), + "alloca_as_pointer" + ); + if (n) + ir.createStore( + consts.ejsval_true(this.triple.pointerSize() == 32), + alloca_as_int64 + ); + else + ir.createStore( + consts.ejsval_false(this.triple.pointerSize() == 32), + alloca_as_int64 + ); + }); + rv._ejs_returns_ejsval_bool = true; + return rv; + } + + loadDoubleEjsValue(n: number): llvm.Value { + // -0 stringifies as "0": without the special case it would share + // +0's cache slot (whichever the function emits first wins, and + // 1/x flips sign — found by the optimizer's neg-of-const fold). + // The test is 1/n === -Infinity, NOT `n === 0 && 1/n < 0`: under + // the self-hosted runtime `-0 === 0` is false (the strict_eq + // tag-compare quirk, math2.js), which silently disabled the + // special case exactly where it mattered. + const key = 1 / n === -Infinity ? "num_-0" : `num_${n}`; + return this.loadCachedEjsValue(key, (alloca) => this.storeDouble(alloca, n)); + } + loadNullEjsValue(): llvm.Value { + return this.loadCachedEjsValue("null", (alloca) => this.storeNull(alloca)); + } + loadUndefinedEjsValue(): llvm.Value { + return this.loadCachedEjsValue("undef", (alloca) => this.storeUndefined(alloca)); + } + + storeUndefined(alloca: llvm.AllocaInst, name?: string): llvm.Value { + let alloca_as_int64 = ir.createBitCast( + alloca, + types.Int64.pointerTo(), + "alloca_as_pointer" + ); + if (this.triple.pointerSize() === 64) + return ir.createStore( + consts.int64_lowhi(0xfff90000, 0x00000000), + alloca_as_int64, + name + ); + // 32 bit + else + return ir.createStore( + consts.int64_lowhi(0xffffff82, 0x00000000), + alloca_as_int64, + name + ); + } + + storeNull(alloca: llvm.AllocaInst, name?: string): llvm.Value { + let alloca_as_int64 = ir.createBitCast( + alloca, + types.Int64.pointerTo(), + "alloca_as_pointer" + ); + if (this.triple.pointerSize() === 64) + return ir.createStore( + consts.int64_lowhi(0xfffb8000, 0x00000000), + alloca_as_int64, + name + ); + // 32 bit + else + return ir.createStore( + consts.int64_lowhi(0xffffff87, 0x00000000), + alloca_as_int64, + name + ); + } + + storeDouble(alloca: llvm.AllocaInst, jsnum: number, name?: string): llvm.Value { + let c = llvm.ConstantFP.getDouble(jsnum); + let alloca_as_double = ir.createBitCast( + alloca, + types.Double.pointerTo(), + "alloca_as_pointer" + ); + return ir.createStore(c, alloca_as_double, name); + } + + createAlloca(func: llvm.EjsFunction, type: llvm.Type, name: string): llvm.AllocaInst { + let saved_insert_point = ir.getInsertBlock(); + ir.setInsertPointStartBB(func.entry_bb!); + let alloca = ir.createAlloca(type, name); + + // if EjsValue was a pointer value we would be able to use an the llvm gcroot intrinsic here. but with the nan boxing + // we kinda lose out as the llvm IR code doesn't permit non-reference types to be gc roots. + // if type is types.EjsValue + // // EjsValues are rooted + // this.createCall this.llvm_intrinsics.gcroot(), [(ir.createPointerCast alloca, types.Int8Pointer.pointerTo(), 'rooted_alloca'), consts.Null types.Int8Pointer], '' + + ir.setInsertPoint(saved_insert_point); + return alloca; + } + + emitEIRToplevel(n: e.FunctionDeclaration): llvm.EjsFunction { + let insertBlock = ir.getInsertBlock(); + + if (!this.eir_emitter) this.eir_emitter = new EIREmitter(this); + if (!this.eir_emitted) this.eir_emitted = new Map(); + let eir_fns = this.eir_emitted.get(n.eir_module!); + if (!eir_fns) { + eir_fns = this.eir_emitter.emitModule(n.eir_module!); + this.eir_emitted.set(n.eir_module!, eir_fns); + } + // export accessors resolve by name against this map (see + // emitModuleResolution) + this.eir_toplevel_fns = eir_fns; + const target = eir_fns.get(n.eir_main!)!; + + const ir_func = n.ir_func!; + this.currentFunction = ir_func; + let entry_bb = new llvm.BasicBlock("entry", ir_func); + ir_func.entry_bb = entry_bb; // cached-literal helpers want this + ir_func.literalAllocas = Object.create(null); + ir_func.topScope = new Map(); + + let body_bb = new llvm.BasicBlock("body", ir_func); + ir.setInsertPoint(body_bb); + let args = ir_func.args; + let rv = this.abi.createCall( + ir_func, + target.type, + target, + [args[0]!, args[1]!, args[2]!, args[3]!, args[4]!], + "eir_toplevel_result" + ); + this.abi.createRet(ir_func, rv); + + // emitModuleResolution wires resolve_modules_bb -> body_bb. the + // entry block's branch is emitted THERE, at the very end: the + // cached-literal helpers append their initializing stores to + // entry_bb, and nothing may follow a terminator. + this.resolve_modules_bb = new llvm.BasicBlock("resolve_modules", ir_func); + this.toplevel_body_bb = body_bb; + this.toplevel_function = ir_func; + this.eir_toplevel_entry_bb = entry_bb; + + this.currentFunction = null; + if (insertBlock) ir.setInsertPoint(insertBlock); + return ir_func; + } + + // an EIR-owned function: emit its EIR module (once) and fill this + // function's body with a forwarding call. closure creation and env + // plumbing stay entirely on the legacy side; the thunk just hands the + // builtin arguments through. + createRet(x: llvm.Value): llvm.Value { + //this.createCall this.ejs_runtime.log, [consts.string(ir, `leaving ${this.currentFunction.name}`)], '' + return this.abi.createRet(this.currentFunction!, x); + } + + generateUCS2(id: number, jsstr: string): llvm.GlobalVariable { + let ucsArrayType = llvm.ArrayType.get(types.JSChar, jsstr.length + 1); + let array_data = []; + for (let i = 0, e = jsstr.length; i < e; i++) + array_data.push(consts.jschar(jsstr.charCodeAt(i))); + array_data.push(consts.jschar(0)); + let array = llvm.ConstantArray.get(ucsArrayType, array_data); + let arrayglobal = new llvm.GlobalVariable( + this.module, + ucsArrayType, + `ucs2-${id}`, + array, + false + ); + arrayglobal.setAlignment(8); + return arrayglobal; + } + + generateEJSPrimString(id: number, _len?: number): llvm.GlobalVariable { + let strglobal = new llvm.GlobalVariable( + this.module, + types.EjsPrimString, + `primstring-${id}`, + llvm.Constant.getAggregateZero(types.EjsPrimString), + false + ); + strglobal.setAlignment(8); + return strglobal; + } + + generateEJSValueForString(id: number | string): llvm.GlobalVariable { + let name = `ejsval-${id}`; + let strglobal = new llvm.GlobalVariable( + this.module, + types.EjsValue, + name, + llvm.Constant.getAggregateZero(types.EjsValue), + false + ); + strglobal.setAlignment(8); + let val = this.module.getOrInsertGlobal(name, types.EjsValue); + val.setAlignment(8); + return val; + } + + addStringLiteralInitialization( + name: string, + ucs2: llvm.GlobalVariable, + primstr: llvm.GlobalVariable, + val: llvm.GlobalVariable, + len: number + ): void { + let saved_insert_point = ir.getInsertBlock(); + + ir.setInsertPointStartBB(this.literalInitializationBB); + + let saved_debug_loc; + if (this.options.debug) { + saved_debug_loc = ir.getCurrentDebugLocation(); + ir.setCurrentDebugLocation( + llvm.DebugLoc.get(0, 0, this.literalInitializationDebugInfo!) + ); + } + + let strname = consts.string(ir, name); + + let arg0 = strname; + let arg1 = val; + let arg2 = primstr; + let arg3 = ir.createInBoundsGetElementPointer( + types.JSChar.pointerTo(), + ucs2, + [consts.int32(0), consts.int32(0)], + "ucs2" + ); + + ir.createCall( + this.ejs_runtime.init_string_literal.type, + this.ejs_runtime.init_string_literal, + [arg0, arg1, arg2, arg3, consts.int32(len)], + "" + ); + ir.setInsertPoint(saved_insert_point); + if (this.options.debug) ir.setCurrentDebugLocation(saved_debug_loc!); + } + + getAtom(str: string): llvm.Value { + // check if it's an atom (a runtime library constant) first of all + if (hasOwn.call(this.ejs_atoms, str)) + return this.createEjsValueLoad(this.ejs_atoms[str]!, `${str}_atom_load`); + + // if it's not, we create a constant and embed it in this module + if (!this.module_atoms.has(str)) { + let literalId = this.idgen(); + let ucs2_data = this.generateUCS2(literalId, str); + let primstring = this.generateEJSPrimString(literalId, str.length); + let ejsval = this.generateEJSValueForString(str); + this.module_atoms.set(str, ejsval); + this.addStringLiteralInitialization(str, ucs2_data, primstring, ejsval, str.length); + } + + return this.createEjsValueLoad(this.module_atoms.get(str)!, "literal_load"); + } + + createCall(callee: llvm.EjsFunction, argv: llvm.Value[], callname: string): llvm.Value { + // the module scaffolding this visitor still emits never runs + // inside a protected region; EIR-emitted code manages its own + // invoke/landingpad pairs (see eir/emit.js) + return this.abi.createCall(this.currentFunction!, callee.type, callee, argv, callname); + } + + emitEjsvalFromPtr(ptr: llvm.Value, prefix: string): llvm.Value { + if (this.triple.pointerSize() === 64) { + let fromptr_alloca = this.createAlloca( + this.currentFunction!, + types.EjsValue, + `${prefix}_ejsval` + ); + let intval = ir.createPtrToInt(ptr, types.Int64, `${prefix}_intval`); + let payload = ir.createOr( + intval, + consts.int64_lowhi(0xfffc0000, 0x00000000), + `${prefix}_payload` + ); + let alloca_as_int64 = ir.createBitCast( + fromptr_alloca, + types.Int64.pointerTo(), + `${prefix}_alloca_asptr` + ); + ir.createStore(payload, alloca_as_int64, `${prefix}_store`); + return ir.createLoad(types.EjsValue, fromptr_alloca, `${prefix}_load`); + } else { + throw new Error("emitEjsvalTo not implemented for this case"); + } + } + + getEjsvalBits(arg: llvm.Value): llvm.Value { + const fn = this.currentFunction!; + const bits_alloca = fn.bits_alloca ?? this.createAlloca(fn, types.EjsValue, "bits_alloca"); + + ir.createStore(arg, bits_alloca); + const bits_ptr = ir.createBitCast(bits_alloca, types.Int64.pointerTo(), "bits_ptr"); + if (!fn.bits_alloca) fn.bits_alloca = bits_alloca; + return ir.createLoad(types.Int64, bits_ptr, "bits_load"); + } + + createEjsvalICmpULt(arg: llvm.Value, i64_const: llvm.Constant, name: string): llvm.Value { + return ir.createICmpULt(this.getEjsvalBits(arg), i64_const, name); + } + // The low tier's NaN-box transfers. Doubles are stored RAW in the + // ejsval (see storeDouble): unbox/box are pure bit reinterpretations + // through the same cached alloca getEjsvalBits uses. Target layout + // knowledge stays here, beside isNumber. + unboxDouble(val: llvm.Value): llvm.Value { + const fn = this.currentFunction!; + const alloca = fn.bits_alloca ?? this.createAlloca(fn, types.EjsValue, "bits_alloca"); + ir.createStore(val, alloca); + const dbl_ptr = ir.createBitCast(alloca, types.Double.pointerTo(), "dbl_ptr"); + if (!fn.bits_alloca) fn.bits_alloca = alloca; + return ir.createLoad(types.Double, dbl_ptr, "unboxed_f64"); + } + boxDouble(dbl: llvm.Value): llvm.Value { + const fn = this.currentFunction!; + const alloca = fn.bits_alloca ?? this.createAlloca(fn, types.EjsValue, "bits_alloca"); + const dbl_ptr = ir.createBitCast(alloca, types.Double.pointerTo(), "dbl_ptr"); + ir.createStore(dbl, dbl_ptr); + if (!fn.bits_alloca) fn.bits_alloca = alloca; + return ir.createLoad(types.EjsValue, alloca, "boxed_f64"); + } + isNumber(val: llvm.Value): llvm.Value { + if (this.triple.pointerSize() === 64) { + return this.createEjsvalICmpULt( + val, + consts.int64_lowhi(0xfff80001, 0x00000000), + "cmpresult" + ); + } else { + let trunc = ir.createTrunc(this.getEjsvalBits(val), types.Int32, "trunc.i"); + return ir.createICmpEq(trunc, consts.int32(-127), "cmpresult"); + } + } + + // shape-guard target-layout helpers (beside isNumber so all + // NaN-box knowledge stays in one place) + + // EJSVAL_IS_OBJECT: object is the topmost shifted tag, so on 64-bit a + // single unsigned compare suffices (mirrors EJSVAL_IS_OBJECT_IMPL) + isObject(val: llvm.Value): llvm.Value { + if (this.triple.pointerSize() === 64) { + return ir.createICmpUGE( + this.getEjsvalBits(val), + consts.int64_lowhi(0xfffc8000, 0x00000000), + "isobj" + ); + } else { + // 32-bit: tag compare, the isNumber trunc convention + let trunc = ir.createTrunc(this.getEjsvalBits(val), types.Int32, "trunc.i"); + return ir.createICmpEq(trunc, consts.int32(-119) /* 0xFFFFFF89 */, "isobj"); + } + } + + // EJSVAL_TO_OBJECT: payload-mask the bits and reinterpret as EJSObject*. + // Only valid under a passed isObject check. + objectPointer(val: llvm.Value): llvm.Value { + if (this.triple.pointerSize() !== 64) + throw new Error("objectPointer not implemented for 32-bit targets"); + const payload = ir.createAnd( + this.getEjsvalBits(val), + consts.int64_lowhi(0x00007fff, 0xffffffff), + "obj_payload" + ); + return ir.createIntToPtr(payload, types.EjsObject.pointerTo(), "objptr"); + } + + // is this value's payload inside the nursery? The seam + // contract (ejs-gc.h EJSHeapContext) fixes the layout: 18 i64 words — + // bump[5], limit[5], nursery_base (word 10), nursery_end (word 11). + // A double's payload can false-positive into the range; the out-of- + // line barrier re-filters, so the inline check only needs to be + // sound-when-true-called. With the nursery off both bounds are 0 + // and the check is constant-false. + heap_ctx_global: llvm.GlobalVariable | null = null; + heapContextGlobal(): llvm.GlobalVariable { + if (!this.heap_ctx_global) + this.heap_ctx_global = new llvm.GlobalVariable( + this.module, + llvm.ArrayType.get(types.Int64, 18), + "_ejs_heap", + null, + true + ); + return this.heap_ctx_global; + } + // the runtime's accessor epoch (ejs-object.h): zero while nothing + // user-installed can intercept a [[Set]] through a fresh object's + // prototype chain. The check is one load + compare-to-zero. + accessor_epoch_global: llvm.GlobalVariable | null = null; + emitAccessorEpochCheck(): llvm.Value { + if (!this.accessor_epoch_global) + this.accessor_epoch_global = new llvm.GlobalVariable( + this.module, + types.Int64, + "_ejs_accessor_epoch", + null, + true + ); + const epoch = ir.createLoad(types.Int64, this.accessor_epoch_global, "accessor_epoch"); + return ir.createICmpEq(epoch, consts.int64(0), "epoch_ok"); + } + // the inline nursery allocation for closure + // environments — bump, compare, init header/length/slots, box with + // the CLOSUREENV tag; the slow thunk (the existing runtime call) is + // the safepoint. With the nursery off, bump/limit are NULL and the + // compare always routes slow. All layout knowledge (cell classes, + // header bits, NaN-box tags, struct offsets) stays here with the + // other NaN-box helpers. + emitEnvAllocInline(n: number, slowCall: () => llvm.Value): llvm.Value { + if (this.triple.pointerSize() !== 64) return slowCall(); + const value_size = 16 + 8 * n; // EJSClosureEnv: u64 header, u32 length(+pad), slots + let cell_size = 16; + while (cell_size < value_size) cell_size *= 2; + if (cell_size > 256) return slowCall(); // LOS-routed sizes take the runtime path + const idx = Math.log2(cell_size) - 4; // seam word: bump[idx], limit[5+idx] + + const g = this.heapContextGlobal(); + const arr_ty = llvm.ArrayType.get(types.Int64, 18); + const bump_p = ir.createInBoundsGetElementPointer( + arr_ty, g, [consts.int64(0), consts.int32(idx)], "env_bump_p"); + const limit_p = ir.createInBoundsGetElementPointer( + arr_ty, g, [consts.int64(0), consts.int32(5 + idx)], "env_limit_p"); + const bump = ir.createLoad(types.Int64, bump_p, "env_bump"); + const limit = ir.createLoad(types.Int64, limit_p, "env_limit"); + // the bindings carry no integer add: pointer arithmetic happens + // through i8 GEPs off the bump address + const i8 = llvm.Type.getInt8Ty(); + const bump_ptr = ir.createIntToPtr(bump, i8.pointerTo(), "env_bump_ptr"); + const byteOffset = (k: number, name: string): llvm.Value => + ir.createInBoundsGetElementPointer(i8, bump_ptr, [consts.int64(k)], name); + const newbump = ir.createPtrToInt( + byteOffset(cell_size, "env_newbump_ptr"), types.Int64, "env_newbump"); + // newbump <= limit, spelled with the comparison the bindings have + const fits = ir.createICmpUGE(limit, newbump, "env_fits"); + + const fast_bb = new llvm.BasicBlock("env_alloc_fast", this.currentFunction!); + const slow_bb = new llvm.BasicBlock("env_alloc_slow", this.currentFunction!); + const join_bb = new llvm.BasicBlock("env_alloc_join", this.currentFunction!); + const from_bb = ir.getInsertBlock()!; + ir.createCondBr(fits, fast_bb, slow_bb); + + ir.setInsertPoint(fast_bb); + ir.createStore(newbump, bump_p); + // header: EJS_SCAN_TYPE_CLOSUREENV | YOUNG (bit 57) + const hdr_ptr = ir.createBitCast(bump_ptr, types.Int64.pointerTo(), "env_hdr_p"); + ir.createStore(consts.int64_lowhi(0x02000000, 0x00000008), hdr_ptr); + // length at +8 (u32) + const len_ptr = ir.createBitCast( + byteOffset(8, "env_len_addr"), types.Int32.pointerTo(), "env_len_p"); + ir.createStore(consts.int32(n), len_ptr); + // slots at +16: undefined-filled, exactly what _ejs_closure_init does + const undef = this.loadUndefinedEjsValue(); + for (let i = 0; i < n; i++) { + const s_ptr = ir.createBitCast( + byteOffset(16 + 8 * i, `env_slot${i}_addr`), + types.EjsValue.pointerTo(), `env_slot${i}_p`); + ir.createStore(undef, s_ptr); + } + // box: CLOSUREENV shifted tag (0x1FFF6 << 47) + const boxed_bits = ir.createOr( + bump, consts.int64_lowhi(0xfffb0000, 0x00000000), "env_boxed_bits"); + const box_alloca = this.createAlloca(this.currentFunction!, types.EjsValue, "env_box"); + const box_i64p = ir.createBitCast(box_alloca, types.Int64.pointerTo(), "env_box_i64p"); + ir.createStore(boxed_bits, box_i64p); + const fast_env = ir.createLoad(types.EjsValue, box_alloca, "env_fast"); + const fast_end_bb = ir.getInsertBlock()!; + ir.createBr(join_bb); + + ir.setInsertPoint(slow_bb); + const slow_env = slowCall(); + const slow_end_bb = ir.getInsertBlock()!; + ir.createBr(join_bb); + + ir.setInsertPoint(join_bb); + const phi = ir.createPhi(types.EjsValue, 2, "env_alloc"); + phi.addIncoming(fast_env, fast_end_bb); + phi.addIncoming(slow_env, slow_end_bb); + return phi; + } + + emitYoungCheck(val: llvm.Value): llvm.Value { + if (this.triple.pointerSize() !== 64) + throw new Error("emitYoungCheck not implemented for 32-bit targets"); + const g = this.heapContextGlobal(); + const arr_ty = llvm.ArrayType.get(types.Int64, 18); + const base_p = ir.createInBoundsGetElementPointer( + arr_ty, g, [consts.int64(0), consts.int32(10)], "nursery_base_p"); + const base = ir.createLoad(types.Int64, base_p, "nursery_base"); + const end_p = ir.createInBoundsGetElementPointer( + arr_ty, g, [consts.int64(0), consts.int32(11)], "nursery_end_p"); + const end = ir.createLoad(types.Int64, end_p, "nursery_end"); + const payload = ir.createAnd( + this.getEjsvalBits(val), + consts.int64_lowhi(0x00007fff, 0xffffffff), + "wb_payload" + ); + const ge = ir.createICmpUGE(payload, base, "wb_ge_base"); + const lt = ir.createICmpULt(payload, end, "wb_lt_end"); + return ir.createAnd(ge, lt, "wb_young"); + } + + // the gc-frame chain head is word 17 of the seam + // (EJSHeapContext.gc_frame_head — bump[5], limit[5], nursery + // bounds, remset words, current_stack_end, priv precede it) + gcFrameHeadPtr(): llvm.Value { + const g = this.heapContextGlobal(); + const arr_ty = llvm.ArrayType.get(types.Int64, 18); + return ir.createInBoundsGetElementPointer( + arr_ty, g, [consts.int64(0), consts.int32(17)], "gc_frame_head_p"); + } + + // link an emitted function's gc-frame record: { prev, count, + // slots[count] } laid out as i64 words in `frame` (an alloca). + // Every slot is initialized to undefined — a stale slot must still + // parse as a valid ejsval when the collector walks it. Linking the + // frame's address into the exported seam is also what makes the + // alloca ESCAPE: LLVM can no longer forward pre-call slot stores to + // post-call reloads across any external call (the + // store-to-load-forwarding hazard the gc plan names). + emitGCFrameLink(frame: llvm.Value, nslots: number, undef: llvm.Value): void { + const i8 = llvm.Type.getInt8Ty(); + const base = ir.createBitCast(frame, i8.pointerTo(), "gcf_base"); + const headp = this.gcFrameHeadPtr(); + const prev = ir.createLoad(types.Int64, headp, "gcf_prev"); + const prev_p = ir.createBitCast(base, types.Int64.pointerTo(), "gcf_prev_p"); + ir.createStore(prev, prev_p); + const count_p = ir.createBitCast( + ir.createInBoundsGetElementPointer(i8, base, [consts.int64(8)], "gcf_count_addr"), + types.Int64.pointerTo(), "gcf_count_p"); + ir.createStore(consts.int64(nslots), count_p); + for (let i = 0; i < nslots; i++) { + const slot_p = ir.createBitCast( + ir.createInBoundsGetElementPointer( + i8, base, [consts.int64(16 + 8 * i)], `gcf_slot${i}_addr`), + types.EjsValue.pointerTo(), `gcf_slot${i}_p`); + ir.createStore(undef, slot_p); + } + ir.createStore(ir.createPtrToInt(base, types.Int64, "gcf_addr"), headp); + } + + // epilogue: pop this frame off the chain + emitGCFrameUnlink(frame: llvm.Value): void { + const i8 = llvm.Type.getInt8Ty(); + const base = ir.createBitCast(frame, i8.pointerTo(), "gcf_base"); + const prev_p = ir.createBitCast(base, types.Int64.pointerTo(), "gcf_prev_p"); + const prev = ir.createLoad(types.Int64, prev_p, "gcf_prev"); + ir.createStore(prev, this.gcFrameHeadPtr()); + } + + // catch handler: the unwind discarded every callee frame below us — + // re-link our own record as the head + emitGCFrameRelink(frame: llvm.Value): void { + const i8 = llvm.Type.getInt8Ty(); + const base = ir.createBitCast(frame, i8.pointerTo(), "gcf_base"); + ir.createStore( + ir.createPtrToInt(base, types.Int64, "gcf_addr"), this.gcFrameHeadPtr()); + } + + // the address of gc-frame slot i, as an EjsValue* + gcFrameSlotPtr(frame: llvm.Value, i: number): llvm.Value { + const i8 = llvm.Type.getInt8Ty(); + const base = ir.createBitCast(frame, i8.pointerTo(), "gcf_base"); + return ir.createBitCast( + ir.createInBoundsGetElementPointer( + i8, base, [consts.int64(16 + 8 * i)], `gcf_slot${i}_addr`), + types.EjsValue.pointerTo(), `gcf_slot${i}_p`); + } + + // inline closure-env slot addressing — payload mask + + // byte offset (EJSClosureEnv: u64 header, u32 length(+pad), slots + // at +16). Recomputed PER USE from the boxed env value, never + // cached across a safepoint: when the env value itself lives in a + // gc-frame slot, the post-safepoint reload feeds a fresh address + // computation, so a relocated env re-derives correctly. Replaces a + // runtime call per env access. + emitEnvSlotRef(env: llvm.Value, slot: number): llvm.Value { + const i8 = llvm.Type.getInt8Ty(); + const payload = ir.createAnd( + this.getEjsvalBits(env), + consts.int64_lowhi(0x00007fff, 0xffffffff), + "env_payload" + ); + const base = ir.createIntToPtr(payload, i8.pointerTo(), "env_base"); + return ir.createBitCast( + ir.createInBoundsGetElementPointer( + i8, base, [consts.int64(16 + 8 * slot)], "env_slot_addr"), + types.EjsValue.pointerTo(), "env_slot_p"); + } + + // the module's i32 shape-index global for `key`, minted on first use + // (initialized to EJS_SHAPE_NOMATCH so a guard can never pass before + // module init interns the real index) + moduleShapeGlobal( + key: string, + fields: { name: string; repr: string }[] + ): llvm.GlobalVariable { + let entry = this.module_shapes.get(key); + if (!entry) { + const g = new llvm.GlobalVariable( + this.module, + types.Int32, + `ejs_shape-${this.idgen()}`, + consts.int32(0xffffff) /* EJS_SHAPE_NOMATCH */, + false + ); + entry = { global: g, fields: fields.slice() }; + this.module_shapes.set(key, entry); + } + return entry.global; + } + + // flush the pending shape interns into their own init function (one + // _ejs_shape_intern call per shape), called by emitModuleResolution + // right after the literal-initialization call — so every atom the + // shapes name is initialized first. A separate function rather than + // the literal-init one: getAtom on a not-yet-interned name restores + // the builder to the END of the current block, which inside an + // already-terminated block would emit past the terminator; here the + // body block stays unterminated until the very end. Called once, + // after all EIR emission. + emitShapeInterns(): llvm.EjsFunction | null { + if (this.module_shapes.size === 0) return null; + const saved_insert = ir.getInsertBlock(); + const saved_function = this.currentFunction; + + const fname = `_ejs_module_init_shapes_${this.filename}`; + const fn = this.module.getOrInsertFunction(fname, types.Void, []); + fn.setInternalLinkage(); + this.currentFunction = fn; + const body_bb = new llvm.BasicBlock("entry", fn); + ir.setInsertPoint(body_bb); + + for (const entry of this.module_shapes.values()) { + const fields = entry.fields; + const arr_ty = llvm.ArrayType.get(types.EjsValue, fields.length); + const arr = ir.createAlloca(arr_ty, "shape_names"); + arr.setAlignment(8); + let f64_mask = 0; + for (let i = 0; i < fields.length; i++) { + if (fields[i]!.repr === "f64") f64_mask |= 1 << i; + const atom = this.getAtom(fields[i]!.name); + const gep = ir.createGetElementPointer( + arr_ty, + arr, + [consts.int32(0), consts.int64(i)], + "shape_name_slot" + ); + ir.createStore(atom, gep); + } + const base = ir.createGetElementPointer( + arr_ty, + arr, + [consts.int32(0), consts.int64(0)], + "shape_names_base" + ); + const idx = this.createCall( + this.ejs_runtime.shape_intern, + [consts.int32(fields.length), base, consts.int32(f64_mask)], + "shape_idx" + ); + ir.createStore(idx, entry.global); + } + ir.createRetVoid(); + + this.currentFunction = saved_function; + if (saved_insert) ir.setInsertPoint(saved_insert); + return fn; + } +} + +function insert_toplevel_func(tree: e.Program, moduleInfo: JSModuleInfo): e.Program { + let toplevel = { + type: b.FunctionDeclaration, + id: b.identifier(moduleInfo.toplevel_function_name), + displayName: "toplevel", + params: [], + defaults: [], + body: { + type: b.BlockStatement, + body: tree.body, + loc: { + start: { + line: 0, + column: 0, + }, + }, + }, + toplevel: true, + generator: false, + expression: false, + loc: { + start: { + line: 0, + column: 0, + }, + }, + }; + + tree.body = [toplevel]; + return tree; +} + +export function compile( + tree: e.Program, + base_output_filename: string, + source_filename: string, + module_infos: Map, + options: CompilerOptions, + triple: Triple +): llvm.Module { + let abi = triple.abi(); + + types.initTypes(triple.pointerSize() === 32); + + let module_filename = source_filename; + + if (module_filename.endsWith(".js")) { + module_filename = module_filename.substring(0, module_filename.length - 3); + } + + const this_module_info = module_infos.get(module_filename) as JSModuleInfo; + + tree = insert_toplevel_func(tree, this_module_info); + + // pipeline-agnostic desugars run before EIR collection so both + // pipelines see their %-intrinsic output + tree = pre_eir_convert(tree, module_filename, module_infos, options); + + // --types (the MAAM oracle): type analysis over the desugared + // toplevel. Must run before collectEIRToplevel, which consumes (and + // then empties) the toplevel body. Logs stats (and, for --types-dump, + // per-binding types); the returned TypeOracle is not consumed by + // codegen unless --types feeds the oracle onward; never fails the compile. + let type_oracle = null; + if (options.types || options.types_dump) + type_oracle = runTypeAnalysisProbe(tree, source_filename, options.types_dump); + + // EIR is the only pipeline: a module that can't lower is a compile + // error, not a fallback + let lowered = collectEIRToplevel( + tree, + source_filename, + module_infos, + this_module_info, + options, + type_oracle + ); + if (lowered.error) throw new Error(`${source_filename}: ${lowered.error}`); + // telemetry: how many guarded diamonds lowering emitted, and + // whether any oracle query missed (the node-identity canary) + if (type_oracle) { + // shape-guard telemetry (visible degradation): + // counted decline reasons, additive-only on the scraped line + const declined = lowered.shape_declined ?? {}; + const declineStr = Object.keys(declined) + .sort() + .map((k) => `${k}:${declined[k]}`) + .join(","); + console.warn( + `--types: ${source_filename}: diamonds=${lowered.diamonds ?? 0} ` + + `oracleQueries=${type_oracle.stats.queries} oracleUnknown=${type_oracle.stats.unknown}` + + // specialization telemetry, present only when it ran + (lowered.spec + ? ` specialized=${lowered.spec.specialized} specSites=${lowered.spec.sites}` + + ` specRejected=${lowered.spec.rejected}` + + // boundary-wrapper telemetry (additive) + (lowered.spec.wrapped > 0 ? ` specWrapped=${lowered.spec.wrapped}` : "") + + (lowered.spec.fenced > 0 ? ` specFenced=${lowered.spec.fenced}` : "") + : "") + + // shape telemetry, present only when sites were consulted + ((lowered.shape_sites ?? 0) > 0 + ? ` shapeSites=${lowered.shape_sites} shapeGuards=${lowered.shape_guards ?? 0}` + + // poly-chain telemetry (additive) + ((lowered.shape_poly_guards ?? 0) > 0 + ? ` shapePolyGuards=${lowered.shape_poly_guards}` + : "") + + ` shapeDeclined=${declineStr || "none"}` + : "") + + // typed slot telemetry (additive) + ((lowered.typed_loads ?? 0) > 0 || (lowered.typed_stores ?? 0) > 0 + ? ` shapeTyped=loads:${lowered.typed_loads ?? 0},stores:${lowered.typed_stores ?? 0}` + : "") + + // born-with-shape telemetry (additive) + ((lowered.born_shaped ?? 0) > 0 || (lowered.ctor_fills ?? 0) > 0 + ? ` bornShaped=${lowered.born_shaped ?? 0} ctorFills=${lowered.ctor_fills ?? 0}` + : "") + + (Object.keys(lowered.fence_declined ?? {}).length > 0 + ? ` fenceDeclined=${Object.keys(lowered.fence_declined!) + .sort() + .map((k) => `${k}:${lowered.fence_declined![k]}`) + .join(",")}` + : "") + + // constructor-result sinking telemetry (additive) + ((lowered.ctor_sunk ?? 0) > 0 ? ` ctorSunk=${lowered.ctor_sunk}` : "") + ); + } + + const toplevel_node = tree.body[0] as e.FunctionDeclaration; + const toplevel_name = toplevel_node.id.name; + + let module = new llvm.Module(base_output_filename); + module.setTriple(triple.llvmTriple()); + module.setDataLayout(triple.dataLayout()); + + (module as unknown as { toplevel_name: string }).toplevel_name = toplevel_name; + + let dibuilder: llvm.DIBuilder | undefined; + let difile: llvm.DIFile | undefined; + + if (options.debug) { + dibuilder = new llvm.DIBuilder(module); + difile = dibuilder.createFile(source_filename + ".js", process.cwd()); + + dibuilder.createCompileUnit(source_filename + ".js", process.cwd(), "ejs", true, "", 2); + } + + // the toplevel's own llvm function — the module-scaffolding wrapper + // that emitEIRToplevel fills and emitModuleResolution finishes + toplevel_node.ir_name = toplevel_name; + toplevel_node.ir_func = types.takes_builtins( + abi.createFunction( + module, + toplevel_name, + abi.ejs_return_type, + abi.ejs_params.map((param) => param.llvm_type) + ) + ); + if (dibuilder && difile) + toplevel_node.ir_func.debug_info = dibuilder.createFunction( + difile, + toplevel_name, + "toplevel", + difile, + 0, + false, + true, + 0, + 0, + true, + toplevel_node.ir_func + ); + + let visitor = new LLVMIRVisitor( + module, + source_filename, + triple, + options, + abi, + module_infos, + this_module_info, + dibuilder, + difile + ); + + if (options.debug) dibuilder!.finalize(); + + visitor.emitModuleInfo(); + + visitor.emitEIRToplevel(toplevel_node); + + // every has_shape has been emitted by now; flush the module's shape + // interns into their init function — + // emitModuleResolution calls it after literal initialization + visitor.shape_init_function = visitor.emitShapeInterns(); + + visitor.emitModuleResolution(lowered.accessors!); + + return module; +} diff --git a/lib/consts.js b/lib/consts.js deleted file mode 100644 index 1e966ea5..00000000 --- a/lib/consts.js +++ /dev/null @@ -1,60 +0,0 @@ -/* -*- Mode: js2; indent-tabs-mode: nil; tab-width: 4; js2-indent-offset: 4; js2-basic-offset: 4; -*- - * vim: set ts=4 sw=4 et tw=99 ft=js: - */ - -import * as types from "./types"; -import * as llvm from "@llvm"; - -export function string(ir, c) { - let constant = ir.createGlobalStringPtr(c, "strconst"); - constant.is_constant = true; - constant.constant_val = c; - return constant; -} - -function intConstant(type, ...constant_val) { - let constant = llvm.Constant.getIntegerValue(type, ...constant_val); - constant.is_constant = true; - constant.constant_val = constant_val; - return constant; -} - -export function jschar(c) { - return intConstant(types.JSChar, c); -} -export function int32(c) { - return intConstant(types.Int32, c); -} -export function int1(c) { - return intConstant(types.Int1, c); -} -export function int64(c) { - return intConstant(types.Int64, c); -} -export function int64_lowhi(ch, cl) { - return intConstant(types.Int64, ch, cl); -} -export function bool(c) { - let constant = llvm.Constant.getIntegerValue(types.Bool, c === false ? 0 : 1); - constant.is_constant = true; - constant.constant_val = c; - return constant; -} - -export function Null(t) { - return llvm.Constant.getNull(t); -} - -export function True() { - return bool(true); -} -export function False() { - return bool(false); -} - -export function ejsval_true(is32bit) { - return int64_lowhi(is32bit ? 0xffffff83 : 0xfff98000, 0x00000001); -} -export function ejsval_false(is32bit) { - return int64_lowhi(is32bit ? 0xffffff83 : 0xfff98000, 0x00000000); -} diff --git a/lib/consts.ts b/lib/consts.ts new file mode 100644 index 00000000..da2ac0b9 --- /dev/null +++ b/lib/consts.ts @@ -0,0 +1,65 @@ +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: + */ + +import * as types from "./types"; +import * as llvm from "@llvm"; + +// the IRBuilder surface string() needs (avoids importing the whole thing) +interface StringBuilder { + createGlobalStringPtr(value: string, name: string): llvm.Constant; +} + +export function string(ir: StringBuilder, c: string): llvm.Constant { + const constant = ir.createGlobalStringPtr(c, "strconst"); + constant.is_constant = true; + constant.constant_val = c; + return constant; +} + +function intConstant(type: llvm.Type, ...constant_val: number[]): llvm.Constant { + const constant = llvm.Constant.getIntegerValue(type, ...constant_val); + constant.is_constant = true; + constant.constant_val = constant_val; + return constant; +} + +export function jschar(c: number): llvm.Constant { + return intConstant(types.JSChar, c); +} +export function int32(c: number): llvm.Constant { + return intConstant(types.Int32, c); +} +export function int1(c: number): llvm.Constant { + return intConstant(types.Int1, c); +} +export function int64(c: number): llvm.Constant { + return intConstant(types.Int64, c); +} +export function int64_lowhi(ch: number, cl: number): llvm.Constant { + return intConstant(types.Int64, ch, cl); +} +export function bool(c: boolean): llvm.Constant { + const constant = llvm.Constant.getIntegerValue(types.Bool, c === false ? 0 : 1); + constant.is_constant = true; + constant.constant_val = c; + return constant; +} + +export function Null(t: llvm.Type): llvm.Constant { + return llvm.Constant.getNull(t); +} + +export function True(): llvm.Constant { + return bool(true); +} +export function False(): llvm.Constant { + return bool(false); +} + +export function ejsval_true(is32bit: boolean): llvm.Constant { + return int64_lowhi(is32bit ? 0xffffff83 : 0xfff98000, 0x00000001); +} +export function ejsval_false(is32bit: boolean): llvm.Constant { + return int64_lowhi(is32bit ? 0xffffff83 : 0xfff98000, 0x00000000); +} diff --git a/lib/debug.js b/lib/debug.js deleted file mode 100644 index e92773db..00000000 --- a/lib/debug.js +++ /dev/null @@ -1,50 +0,0 @@ -/* -*- Mode: js2; indent-tabs-mode: nil; tab-width: 4; js2-indent-offset: 4; js2-basic-offset: 4; -*- - * vim: set ts=4 sw=4 et tw=99 ft=js: - */ - -let _indent = 0; -let _debug_level = 0; - -export function log() { - let level = 3; - let msg = null; - - if (arguments.length > 1) { - level = arguments[0]; - msg = arguments[1]; - } else if (arguments.length == 1) { - msg = arguments[0]; - } - - if (_debug_level < level) return; - - if (typeof msg === "function") msg = msg(); - - if (msg) - //console.warn(`${' '.repeat(_indent)}${msg}`); - console.warn(msg); -} - -export function indent() { - _indent += 1; -} -export function unindent() { - _indent -= 1; - if (_indent < 0) { - console.warn("indent level mismatch. setting to 0"); - _indent = 0; - } -} -export function setLevel(x) { - _debug_level = x; -} - -export function time(level, id) { - if (_debug_level < level) return; - console.time(id); -} - -export function timeEnd(level, id) { - if (_debug_level < level) return; - console.timeEnd(id); -} diff --git a/lib/debug.ts b/lib/debug.ts new file mode 100644 index 00000000..17105a53 --- /dev/null +++ b/lib/debug.ts @@ -0,0 +1,55 @@ +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: + */ + +type Message = string | (() => string); + +let _indent = 0; +let _debug_level = 0; + +export function log(msg: Message): void; +export function log(level: number, msg: Message): void; +export function log(levelOrMsg: number | Message, maybeMsg?: Message): void { + let level: number; + let msg: Message; + + if (maybeMsg !== undefined) { + level = levelOrMsg as number; + msg = maybeMsg; + } else { + level = 3; + msg = levelOrMsg as Message; + } + + if (_debug_level < level) return; + + const text = typeof msg === "function" ? msg() : msg; + + if (text) console.warn(text); +} + +export function indent(): void { + _indent += 1; +} + +export function unindent(): void { + _indent -= 1; + if (_indent < 0) { + console.warn("indent level mismatch. setting to 0"); + _indent = 0; + } +} + +export function setLevel(x: number): void { + _debug_level = x; +} + +export function time(level: number, id: string): void { + if (_debug_level < level) return; + console.time(id); +} + +export function timeEnd(level: number, id: string): void { + if (_debug_level < level) return; + console.timeEnd(id); +} diff --git a/lib/desugar.ts b/lib/desugar.ts new file mode 100644 index 00000000..fcc29658 --- /dev/null +++ b/lib/desugar.ts @@ -0,0 +1,98 @@ +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: + */ + +import { DesugarClasses } from "./passes/desugar-classes"; +import { DesugarDestructuring } from "./passes/desugar-destructuring"; +import { DesugarGeneratorFunctions } from "./passes/desugar-generator-functions"; +import { DesugarSpread } from "./passes/desugar-spread"; +import { DesugarMetaProperties } from "./passes/desugar-metaproperties"; +import { HoistFuncDecls } from "./passes/hoist-func-decls"; +import { TransformPass } from "./node-visitor"; + +import * as escodegen from "../external-deps/escodegen/escodegen-es6"; +import * as debug from "./debug"; + +import type { Program } from "./estree"; +import type { CompilerOptions } from "./options"; +import type { ModuleInfo } from "./module-info"; + +type PassConstructor = new ( + options: CompilerOptions, + filename: string, + modules: Map +) => TransformPass; + +// the AST->AST desugar passes that run before EIR collection: constructs +// EIR has no native lowering for arrive there as %-intrinsic calls, which +// lower through lib/eir/intrinsics.ts. +// +// DesugarClasses, then DesugarDestructuring, then +// DesugarGeneratorFunctions, then DesugarSpread: super(...args) desugars +// into %constructSuper(ref, ...args) first, patterns unfold into +// member/iterator reads, generator methods desugar as plain function +// expressions, and the spread pass then rewrites what remains. +// +// HoistFuncDecls hoists last: nothing after it (spread/meta emit no +// function declarations) re-creates block-level decls. it gives v8 +// semantics — block-level declarations hoist to function scope, and +// same-name redeclarations collapse to the last one; at the toplevel it +// also moves the closure slot stores to the top, where hoisting says +// they belong. +const pre_eir_passes: PassConstructor[] = [ + DesugarClasses, + DesugarDestructuring, + DesugarGeneratorFunctions, + DesugarSpread, + DesugarMetaProperties, + HoistFuncDecls, +]; + +// the self-hosted runtime exposes GC statistics through a global +declare const __ejs: + | { GC: { dumpAllocationStats(tag: string): void } } + | undefined; + +function runPasses( + passList: PassConstructor[], + tree: Program, + filename: string, + modules: Map, + options: CompilerOptions +): Program { + for (const passType of passList) { + try { + debug.time(2, passType.name); + const pass = new passType(options, filename, modules); + tree = pass.visit(tree) as Program; + debug.timeEnd(2, passType.name); + if (options.debug_passes.has(passType.name)) { + console.log(`after: ${passType.name}`); + console.log(escodegen.generate(tree)); + } + + debug.log(2, `after: ${passType.name}`); + debug.log(2, () => escodegen.generate(tree)); + debug.log(3, () => { + if (typeof __ejs != "undefined") __ejs.GC.dumpAllocationStats(`after ${passType.name}`); + return ""; + }); + } catch (e) { + debug.log(2, `exception in pass ${passType.name}`); + debug.log(2, String(e)); + throw e; + } + } + + return tree; +} + +// runs in compile() before collectEIRToplevel +export function preEIRConvert( + tree: Program, + filename: string, + modules: Map, + options: CompilerOptions +): Program { + return runPasses(pre_eir_passes, tree, filename, modules, options); +} diff --git a/lib/echo-util.js b/lib/echo-util.js deleted file mode 100644 index 8cdedbbc..00000000 --- a/lib/echo-util.js +++ /dev/null @@ -1,157 +0,0 @@ -/* -*- Mode: js2; indent-tabs-mode: nil; tab-width: 4; js2-indent-offset: 4; js2-basic-offset: 4; -*- - * vim: set ts=4 sw=4 et tw=99 ft=js: - */ -//let terminal = require('terminal'); - -import * as b from "./ast-builder"; - -export function shallow_copy_object(o) { - if (!o) return null; - - let new_o = Object.create(Object.getPrototypeOf(o)); - for (let x of Object.getOwnPropertyNames(o)) new_o[x] = o[x]; - return new_o; -} - -export function map(f, seq) { - let rv = []; - for (let el of seq) { - rv.push(f(el)); - } - return rv; -} - -export function foldl(f, z, arr) { - if (arr.length === 0) return z; - - return foldl(f, f(z, arr[0]), arr.slice(1)); -} - -export function reject(o, pred) { - let rv = Object.create(null); - for (let prop of Object.getOwnPropertyNames(o)) { - if (!pred(prop)) rv[prop] = o[prop]; - } - return rv; -} - -export function startGenerator() { - let _gen = 0; - return () => { - let id = _gen; - _gen += 1; - return id; - }; -} - -let filenameGenerator = startGenerator(); - -export function genFreshFileName(x) { - return `${x}.${filenameGenerator()}`; -} - -let functionNameGenerator = startGenerator(); - -export function genGlobalFunctionName(x, filename) { - let prefix = filename ? `__ejs[${filename}]` : "__ejs_fn"; - return `${prefix}_${x}_${functionNameGenerator()}`; -} - -export function genAnonymousFunctionName(filename) { - let prefix = filename ? `__ejs[${filename}]_%anon` : "__ejs_%anon"; - return `${prefix}_${functionNameGenerator()}`; -} - -export function bold() { - /* - if (process && process.stderr && process.stderr.isTTY) - return terminal.ANSIStyle('bold'); - */ - return ""; -} - -export function reset() { - /* - if (process && process.stderr && process.stderr.isTTY) - return terminal.ANSIStyle('reset'); - */ - return ""; -} - -export function underline(str) { - return str + "\n" + "-".repeat(str.length); -} - -export function is_number_literal(n) { - return n.type === b.Literal && typeof n.value === "number"; -} -export function is_string_literal(n) { - return n.type === b.Literal && typeof n.raw === "string"; -} -export function create_intrinsic(id, args, loc) { - return { - type: b.CallExpression, - callee: id, - arguments: args, - loc: loc, - }; -} - -export function is_intrinsic(n, name) { - if (n.type !== b.CallExpression) return false; - if (n.callee.type !== b.Identifier) return false; - if (n.callee.name[0] !== "%") return false; - if (name && n.callee.name !== name) return false; - - return true; -} - -export function intrinsic(id, args, loc) { - let rv = b.callExpression(id, args); - rv.loc = loc; - return rv; -} - -export function sanitize_with_regexp(filename) { - return filename.replace(/[.,-\/\\]/g, "_"); // this is insanely inadequate -} - -export class Writer { - constructor(stream) { - this.stream = stream; - this.have_blank_line = true; - } - - write(msg, want_newline = false) { - if (want_newline) { - if (!this.have_blank_line) { - this.stream.write("\n"); - } - } - let out_msg = String(msg); - if (this.stream.isTTY && this.stream.columns > 0) { - let cols = this.stream.columns; - if (out_msg.length >= cols) { - // we should be awesome here and elide something from - // the middle of the line - let elide_length = out_msg.length - cols + 5; - if (elide_length < 0) { - // XXX something more here... - out_msg = out_msg.substr(0, cols); - } else { - let elide_start = out_msg.length / 2 - elide_length / 2; - let elide_end = out_msg.length / 2 + elide_length / 2; - - out_msg = out_msg.slice(0, elide_start) + " ... " + out_msg.slice(elide_end); - } - } else { - out_msg = out_msg + " ".repeat(cols - out_msg.length); - } - this.stream.write("\r"); - this.stream.write(out_msg); - this.have_blank_line = false; - } else { - this.stream.write(out_msg + "\n"); - } - } -} diff --git a/lib/echo-util.ts b/lib/echo-util.ts new file mode 100644 index 00000000..81474ed1 --- /dev/null +++ b/lib/echo-util.ts @@ -0,0 +1,118 @@ +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: + */ + +import * as b from "./ast-builder"; +import type { + CallExpression, + Expression, + Identifier, + Node, + SourceLocation, + SpreadElement, +} from "./estree"; + +export function startGenerator(): () => number { + let _gen = 0; + return () => { + const id = _gen; + _gen += 1; + return id; + }; +} + +const filenameGenerator = startGenerator(); + +export function genFreshFileName(x: string): string { + return `${x}.${filenameGenerator()}`; +} + +export function bold(): string { + return ""; +} + +export function reset(): string { + return ""; +} + +export function underline(str: string): string { + return str + "\n" + "-".repeat(str.length); +} + +export function is_string_literal(n: Node): boolean { + return n.type === b.Literal && typeof n.raw === "string"; +} + +// a call whose callee is a %-named identifier is a compiler intrinsic +// (a lowering directive minted by the desugar passes, never user code — +// '%' can't appear in a parsed identifier) +export function is_intrinsic(n: Node, name?: string): boolean { + if (n.type !== b.CallExpression) return false; + if (n.callee.type !== b.Identifier) return false; + if (n.callee.name[0] !== "%") return false; + if (name && n.callee.name !== name) return false; + + return true; +} + +export function intrinsic( + id: Identifier, + args: (Expression | SpreadElement)[], + loc?: SourceLocation | null +): CallExpression { + const rv = b.callExpression(id, args); + rv.loc = loc; + return rv; +} + +export function sanitize_with_regexp(filename: string): string { + return filename.replace(/[.,-/\\]/g, "_"); // this is insanely inadequate +} + +interface WritableStream { + write(msg: string): void; + isTTY?: boolean; + columns?: number; +} + +export class Writer { + stream: WritableStream; + have_blank_line = true; + + constructor(stream: WritableStream) { + this.stream = stream; + } + + write(msg: string, want_newline = false): void { + if (want_newline) { + if (!this.have_blank_line) { + this.stream.write("\n"); + } + } + let out_msg = String(msg); + if (this.stream.isTTY && this.stream.columns && this.stream.columns > 0) { + const cols = this.stream.columns; + if (out_msg.length >= cols) { + // we should be awesome here and elide something from + // the middle of the line + const elide_length = out_msg.length - cols + 5; + if (elide_length < 0) { + // XXX something more here... + out_msg = out_msg.substr(0, cols); + } else { + const elide_start = out_msg.length / 2 - elide_length / 2; + const elide_end = out_msg.length / 2 + elide_length / 2; + + out_msg = out_msg.slice(0, elide_start) + " ... " + out_msg.slice(elide_end); + } + } else { + out_msg = out_msg + " ".repeat(cols - out_msg.length); + } + this.stream.write("\r"); + this.stream.write(out_msg); + this.have_blank_line = false; + } else { + this.stream.write(out_msg + "\n"); + } + } +} diff --git a/lib/eir/builder.ts b/lib/eir/builder.ts new file mode 100644 index 00000000..97b274cd --- /dev/null +++ b/lib/eir/builder.ts @@ -0,0 +1,244 @@ +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: + */ + +// SSA construction (Braun et al., "Simple and Efficient Construction of +// Static Single Assignment Form"): +// - readVariable/writeVariable give the AST lowering a mutable-variable +// view; block parameters materialize on demand at joins. +// - blocks are sealed once all predecessors are known; incomplete +// parameters get their per-edge arguments filled in. +// - trivial parameters (all incoming arguments equal, or only the +// parameter itself) are removed recursively. + +import { Func, Block, Inst, replaceAllUses, usersOf } from "./ir"; +import type { Imms } from "./ir"; +import { opInfo, Effect } from "./ops"; + +export class FunctionBuilder { + fn: Func; + // varname -> (block -> value) + defs = new Map>(); + cur!: Block; + // stack of catch blocks; when non-empty, may-throw instructions get + // explicit normal/unwind edges (invoke style) + handlers: Block[] = []; + + constructor(name: string, paramNames: string[]) { + this.fn = new Func(name, paramNames); + + const entry = this.newBlock("entry"); + this.setInsertPoint(entry); + // function parameters are the entry block's parameters + for (const pname of this.fn.paramNames) { + const p = entry.addParam(pname); + this.writeVariable(pname, entry, p); + } + this.sealBlock(entry); + } + + newBlock(name?: string): Block { + return this.fn.addBlock(new Block(this.fn, name)); + } + + setInsertPoint(block: Block): void { + this.cur = block; + } + + // --- instruction emission ------------------------------------------------ + + emit(op: string, operands: Inst[], imms: Imms): Inst { + if (this.cur.terminated) + throw new Error(`emitting '${op}' into terminated block ${this.cur.name}`); + const inst = new Inst(this.fn, op, operands, imms); + inst.block = this.cur; + this.cur.insts.push(inst); + + // inside a protected region, a may-throw instruction terminates its + // block with an explicit normal/unwind pair, and insertion continues + // in the normal successor. + const info = opInfo(op); + if (this.handlers.length > 0 && (info.effects & Effect.THROW) !== 0 && !info.terminator) { + const handler = this.handlers[this.handlers.length - 1]!; + const cont = this.newBlock("cont"); + inst.addTarget(cont, [], "normal"); + inst.addTarget(handler, [], "unwind"); + this.sealBlock(cont); + this.setInsertPoint(cont); + } + return inst; + } + + // --- exception handling ----------------------------------------------------- + + newCatchBlock(name?: string): Block { + const block = this.newBlock(name || "catch"); + block.isCatch = true; + const exc = block.addParam("%exception"); + exc.isException = true; + exc.type = "exception"; + return block; + } + + pushHandler(catchBlock: Block): void { + this.handlers.push(catchBlock); + } + + popHandler(): Block | undefined { + return this.handlers.pop(); + } + + // a `throw` statement: unwinds to the active handler if there is one, + // otherwise out of the function. + throwValue(v: Inst): Inst { + const inst = this.emit("throw", [v], {}); + if (this.handlers.length > 0) + inst.addTarget(this.handlers[this.handlers.length - 1]!, [], "unwind"); + return inst; + } + + constNumber(v: number): Inst { + return this.emit("const", [], { kind: "number", value: v }); + } + constAtom(s: string): Inst { + return this.emit("const", [], { kind: "atom", value: s }); + } + constBool(v: boolean): Inst { + return this.emit("const", [], { kind: "boolean", value: v }); + } + constUndefined(): Inst { + return this.emit("const", [], { kind: "undefined" }); + } + constNull(): Inst { + return this.emit("const", [], { kind: "null" }); + } + + br(block: Block, args?: Inst[]): Inst { + const inst = this.emit("br", [], {}); + inst.addTarget(block, args || []); + return inst; + } + + condBr(cond: Inst, tblock: Block, targs: Inst[], fblock: Block, fargs: Inst[]): Inst { + const inst = this.emit("cond_br", [cond], {}); + inst.addTarget(tblock, targs || []); + inst.addTarget(fblock, fargs || []); + return inst; + } + + ret(value: Inst): Inst { + return this.emit("return", [value], {}); + } + + // --- Braun SSA ------------------------------------------------------------- + + writeVariable(name: string, block: Block, value: Inst): void { + let m = this.defs.get(name); + if (!m) { + m = new Map(); + this.defs.set(name, m); + } + m.set(block, value); + } + + hasVariable(name: string): boolean { + return this.defs.has(name); + } + + readVariable(name: string, block: Block): Inst { + const m = this.defs.get(name); + const v = m && m.get(block); + if (v) return v; + return this.readVariableRecursive(name, block); + } + + readVariableRecursive(name: string, block: Block): Inst { + let val: Inst; + if (!block.sealed) { + // incomplete CFG: leave a parameter to be filled at seal time + const param = block.addParam(name); + block.incompleteParams.set(name, param); + val = param; + } else if (block.predEdges.length === 1) { + val = this.readVariable(name, block.predEdges[0]!.inst.block!); + } else if (block.predEdges.length === 0) { + if (block !== this.fn.entry) { + // an unreachable block (code after `while (true)`, after a + // switch whose every case returns, ...): any value will do. + // the emitter drops unreachable blocks entirely. + const c = new Inst(this.fn, "const", [], { kind: "undefined" }); + c.block = block; + block.insts.unshift(c); + val = c; + } else { + throw new Error(`EIR: read of undefined variable '${name}' reached entry`); + } + } else { + // break potential cycles with a parameter before recursing + const param = block.addParam(name); + this.writeVariable(name, block, param); + val = this.addParamOperands(name, param); + } + this.writeVariable(name, block, val); + return val; + } + + addParamOperands(name: string, param: Inst): Inst { + const block = param.block!; + const argIdx = block.argIndexOfParam(param); + for (const e of block.predEdges) { + const predBlock = e.inst.block!; + const v = this.readVariable(name, predBlock); + e.inst.targets![e.targetIndex]!.args[argIdx] = v; + } + return this.tryRemoveTrivialParam(param); + } + + tryRemoveTrivialParam(param: Inst): Inst { + if (param.isException) return param; // produced by unwinding, never trivial + const block = param.block!; + const argIdx = block.argIndexOfParam(param); + let same: Inst | null = null; + for (const e of block.predEdges) { + const arg = e.inst.targets![e.targetIndex]!.args[argIdx]; + if (arg === same || arg === param) continue; + if (same !== null) return param; // merges at least two distinct values: keep it + same = arg ?? null; + } + // unreachable block or self-reference only + if (same === null) return param; + + // collect users before rewriting so we can recheck dependent params + const users = usersOf(this.fn, param).filter((u) => u !== param); + + replaceAllUses(this.fn, param, same); + // fix stale variable definitions that still point at the removed param + for (const m of this.defs.values()) { + for (const entry of m.entries()) { + if (entry[1] === param) m.set(entry[0], same); + } + } + block.removeParam(param); + + for (const u of users) { + if (u.op === "blockparam" && !u.removed) this.tryRemoveTrivialParam(u); + } + return same; + } + + sealBlock(block: Block): void { + if (block.sealed) throw new Error(`sealing already-sealed block ${block.name}`); + block.sealed = true; + for (const entry of block.incompleteParams.entries()) { + this.addParamOperands(entry[0], entry[1]); + } + block.incompleteParams.clear(); + } + + finish(): Func { + for (const b of this.fn.blocks) { + if (!b.sealed) throw new Error(`EIR: ${this.fn.name}: block ${b.name} never sealed`); + } + return this.fn; + } +} diff --git a/lib/eir/cleanup.ts b/lib/eir/cleanup.ts new file mode 100644 index 00000000..754a1fea --- /dev/null +++ b/lib/eir/cleanup.ts @@ -0,0 +1,883 @@ +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: + */ + +// compiler-P1 "optimizer residue": the classic SSA cleanups and the +// type lattice. +// +// (a) a trust-free TYPE LATTICE over the boxed `any` values — +// value-intrinsic tags (const kinds, allocation ops, generic ops +// with fixed result types) met over block-param edges to a +// fixpoint. Nothing here consumes an oracle claim; every tag is +// proven from the IR, so the lattice is sound on flag-off +// compiles too. +// (b) CONSTANT FOLDING over primitive consts, evaluated in the +// hosting engine (both host and target implement the same ES +// semantics for primitive arithmetic/comparison; folds that +// would mint a STRING from a non-string — number formatting — +// are excluded, as are string relational compares, so a host/ +// runtime divergence in either can never be baked in at compile +// time. typeof folds follow the RUNTIME's mapping, including +// its `typeof null == "null"` quirk). +// (c) REDUNDANT to_boolean/typeof ELIMINATION: cond_br on a +// known-truthiness to_boolean folds; `to_boolean(logical_not x)` +// inverts the branch instead of calling _ejs_op_not + _ejs_truthy; +// `typeof x === "T"` becomes the single-tag-test typeof_is op. +// (d) TRIVIAL BLOCK PARAM pruning (the SSA form of copy +// propagation): a param fed the same SSA value on every edge is +// that value (the value dominates every pred, hence the block, +// hence every use of the param). +// (e) LATTICE-TYPED LOW-TIER LOWERING — the "feeding the low-tier +// ops beyond what the oracle already types" item: a generic +// add/sub/mul/div both of whose operands the lattice proves +// number computes bit-identically in f64 (ES semantics; the +// guard-region soundness inventory's argument), so it lowers to +// unbox/f64_*/box with no guard at all. lt/gt lower to f64_lt +// when their only consumer is a same-block to_boolean + cond_br. +// (f) MODULE-SLOT LOAD CSE (the toplevel-receiver reload noted at +// shapes-P3): +// - block-local availability, killed at CALL-effect +// instructions (arbitrary JS may re-enter this module's +// stores) unless the slot is single-store (below), with +// store-to-load forwarding; +// - single-store %self slots: the module init flag is set +// BEFORE the toplevel body runs (compiler.ts +// emitModuleResolution), so the toplevel executes at most +// once per process and a %self slot whose ONLY static store +// sits in the toplevel entry block is immutable once +// written. In the toplevel itself, every load the store +// comes-before folds to the stored value; in any other +// function the slot cannot change during an activation (the +// suspended init's remaining stores can only run after this +// function returns; re-entry is blocked by the flag), so a +// dominated load folds to its dominator. The census counts +// EVERY module_slot_store — including export-accessor +// setters — so an externally-writable binding never +// qualifies. +// +// Pass placement (optimize.ts): CSE runs before the guard/shape region +// passes (receiver identity is what lets toplevel regions merge); the +// folding passes run AFTER them — like foldUnboxOfBox, folding +// arithmetic earlier would perturb the exact IR shapes the region +// matchers verify. + +import { Func, Block, Inst, replaceAllUses } from "./ir"; +import type { Target } from "./ir"; +import { Effect, opInfo } from "./ops"; +import { condBrToBr, sweepUnreachableBlocks } from "./optimize-guards"; +import { computeRPO, computeDominators, dominates } from "./verifier"; +import type { OptStats } from "./optimize"; + +// --- the type lattice ------------------------------------------------------- + +// flat lattice over the boxed value tags: undefined (in the array) is +// bottom (no information yet), "top" is no-information-possible. The +// tags mirror the runtime's tag taxonomy (typeof-null quirk included: +// null is its own tag here AND in _ejs_op_typeof). +export type LatticeTag = + | "number" + | "string" + | "boolean" + | "undefined" + | "null" + | "object" + | "function" + | "top"; + +export type Lattice = (LatticeTag | undefined)[]; + +// generic ops whose result is always a Number (ES: they apply +// ToNumber/ToInt32/ToUint32 and produce a Number or throw; +// runtime/ejs-ops.c agrees — only NUMBER_TO_EJSVAL returns) +const NUMBER_RESULT = new Set([ + "sub", + "mul", + "div", + "mod", + "neg", + "unary_plus", + "bitand", + "bitor", + "bitxor", + "shl", + "shr", + "ushr", + "bitnot", +]); + +// generic ops whose result is always a Boolean +const BOOLEAN_RESULT = new Set([ + "lt", + "le", + "gt", + "ge", + "loose_eq", + "loose_neq", + "strict_eq", + "strict_neq", + "logical_not", + "instanceof", + "in", + "typeof_is", +]); + +// ops that always produce a (non-callable) Object +const OBJECT_RESULT = new Set([ + "make_object", + "make_object_shaped", + "make_array", + "make_regexp", + "args_obj", + "rest_args", + "array_from_spread", + "template_callsite", +]); + +function meet(a: LatticeTag | undefined, b: LatticeTag | undefined): LatticeTag | undefined { + if (a === undefined) return b; + if (b === undefined) return a; + return a === b ? a : "top"; +} + +function constTag(inst: Inst): LatticeTag { + switch (inst.imms["kind"] as string) { + case "number": + return "number"; + case "atom": + return "string"; + case "boolean": + return "boolean"; + case "undefined": + return "undefined"; + case "null": + return "null"; + default: + return "top"; + } +} + +// one evaluation of the transfer function for `inst` under `tags` +function instTag(inst: Inst, tags: Lattice): LatticeTag | undefined { + const op = inst.op; + if (op === "const") return constTag(inst); + if (op === "box_f64") return "number"; + if (NUMBER_RESULT.has(op)) return "number"; + if (BOOLEAN_RESULT.has(op)) return "boolean"; + if (OBJECT_RESULT.has(op)) return "object"; + if (op === "make_closure") return "function"; + if (op === "typeof") return "string"; + if (op === "add") { + // string if either side is a string (ES: a string primitive on + // either side means concatenation); number only if both sides + // are numbers; anything else can go either way (objects' + // ToPrimitive decides at runtime) + const a = tags[inst.operands[0]!.id]; + const b = tags[inst.operands[1]!.id]; + if (a === "string" || b === "string") return "string"; + if (a === undefined || b === undefined) return undefined; + if (a === "number" && b === "number") return "number"; + return "top"; + } + if (op === "blockparam") { + const b = inst.block!; + // entry params are the calling convention's values; exception + // params carry whatever was thrown + if (b === b.fn.entry || inst.isException || b.isCatch) return "top"; + if (b.predEdges.length === 0) return undefined; // unreachable + const argIdx = b.argIndexOfParam(inst); + let t: LatticeTag | undefined = undefined; + for (const e of b.predEdges) { + const arg = e.inst.targets![e.targetIndex]!.args[argIdx]; + if (!arg) return "top"; + if (arg === inst) continue; // self-edge: vacuous + t = meet(t, tags[arg.id]); + if (t === "top") return t; + } + return t; + } + return "top"; +} + +// fixpoint over the whole function. The lattice has height 2, so the +// loop terminates quickly; the bound is belt and braces. +export function computeLattice(fn: Func): Lattice { + const tags: Lattice = new Array(fn.next_value_id); + for (let round = 0; round < 20; round++) { + let changed = false; + fn.forEachInst((inst) => { + const t = instTag(inst, tags); + if (t !== undefined && tags[inst.id] !== t) { + // monotone by construction (undefined -> tag -> top) + tags[inst.id] = t; + changed = true; + } + }); + if (!changed) break; + } + return tags; +} + +// --- constant folding ------------------------------------------------------- + +// the JS payload of a primitive const +function constPayload(inst: Inst): unknown { + switch (inst.imms["kind"] as string) { + case "undefined": + return undefined; + case "null": + return null; + default: + return inst.imms["value"]; + } +} + +// rewrite `inst` in place into a primitive const (same Inst object +// keeps every use — the foldUnboxOfBox precedent) +function toConst(inst: Inst, value: unknown, stats: OptStats): void { + let imms: Inst["imms"]; + if (value === undefined) imms = { kind: "undefined" }; + else if (value === null) imms = { kind: "null" }; + else if (typeof value === "number") imms = { kind: "number", value: value }; + else if (typeof value === "boolean") imms = { kind: "boolean", value: value }; + else imms = { kind: "atom", value: String(value) }; + inst.op = "const"; + inst.operands.length = 0; + inst.imms = imms; + inst.type = "any"; + stats.consts_folded++; +} + +// binops foldable by evaluating the SAME ES semantics in the hosting +// engine. Relational ops are restricted to number operands (string +// relational compare is the one place a host/runtime collation +// difference could hide); results are accepted only when they are +// numbers/booleans, or strings made purely from strings. +const EVAL_BINOPS = new Set([ + "add", + "sub", + "mul", + "div", + "mod", + "bitand", + "bitor", + "bitxor", + "shl", + "shr", + "ushr", + "lt", + "le", + "gt", + "ge", + "loose_eq", + "loose_neq", + "strict_eq", + "strict_neq", +]); + +const RELATIONAL = new Set(["lt", "le", "gt", "ge"]); + +/* eslint-disable @typescript-eslint/no-explicit-any */ +function evalBinop(op: string, x: any, y: any): unknown { + switch (op) { + case "add": + return x + y; + case "sub": + return x - y; + case "mul": + return x * y; + case "div": + return x / y; + case "mod": + return x % y; + case "bitand": + return x & y; + case "bitor": + return x | y; + case "bitxor": + return x ^ y; + case "shl": + return x << y; + case "shr": + return x >> y; + case "ushr": + return x >>> y; + case "lt": + return x < y; + case "le": + return x <= y; + case "gt": + return x > y; + case "ge": + return x >= y; + case "loose_eq": + return x == y; + case "loose_neq": + return x != y; + case "strict_eq": + return x === y; + case "strict_neq": + return x !== y; + default: + return undefined; + } +} + +function evalUnop(op: string, x: any): unknown { + switch (op) { + case "neg": + return -x; + case "unary_plus": + return +x; + case "bitnot": + return ~x; + case "logical_not": + return !x; + default: + return undefined; + } +} +/* eslint-enable @typescript-eslint/no-explicit-any */ + +// the runtime's typeof string for a lattice tag (_ejs_op_typeof — +// spec mapping, typeof null is "object") +const TYPEOF_OF_TAG: Record = { + number: "number", + string: "string", + boolean: "boolean", + undefined: "undefined", + null: "object", + object: "object", + function: "function", +}; + +function foldConstants(fn: Func, tags: Lattice, stats: OptStats): boolean { + let changed = false; + fn.forEachInst((inst) => { + if (inst.targets && inst.targets.length > 0) return; // protected-region terminator + const op = inst.op; + if (op === "typeof") { + const t = tags[inst.operands[0]!.id]; + const s = t && t !== "top" ? TYPEOF_OF_TAG[t] : undefined; + if (s !== undefined) { + toConst(inst, s, stats); + changed = true; + } + return; + } + if (EVAL_BINOPS.has(op)) { + const a = inst.operands[0]!; + const b = inst.operands[1]!; + if (a.op !== "const" || b.op !== "const") return; + if ( + RELATIONAL.has(op) && + (a.imms["kind"] !== "number" || b.imms["kind"] !== "number") + ) + return; + const r = evalBinop(op, constPayload(a), constPayload(b)); + if (typeof r === "number" || typeof r === "boolean") { + toConst(inst, r, stats); + changed = true; + } else if ( + typeof r === "string" && + a.imms["kind"] === "atom" && + b.imms["kind"] === "atom" + ) { + toConst(inst, r, stats); + changed = true; + } + return; + } + if (op === "neg" || op === "unary_plus" || op === "bitnot" || op === "logical_not") { + const a = inst.operands[0]!; + if (a.op !== "const") return; + if (op !== "logical_not" && a.imms["kind"] !== "number") return; + const r = evalUnop(op, constPayload(a)); + if (typeof r === "number" || typeof r === "boolean") { + toConst(inst, r, stats); + changed = true; + } + } + }); + return changed; +} + +// --- typeof_is peephole ----------------------------------------------------- + +// `typeof x === "T"` (either operand order) is a single runtime tag +// test. The rewrite is exact per _ejs_op_typeof's mapping (the +// runtime's typeof_is_ tests the same predicate typeof compares +// against — typeof_is_object admits null, typeof_is_null is constant +// false); the typeof goes dead and DCE sweeps it. Only the types with +// runtime.ts entries qualify. +const TYPEOF_IS_TYPES = new Set([ + "object", + "function", + "string", + "number", + "undefined", + "null", + "boolean", +]); + +function typeofIsPeephole(fn: Func, stats: OptStats): boolean { + let changed = false; + fn.forEachInst((inst) => { + if (inst.op !== "strict_eq") return; + let tof = inst.operands[0]!; + let lit = inst.operands[1]!; + if (tof.op !== "typeof") { + const t = tof; + tof = lit; + lit = t; + } + if (tof.op !== "typeof" || tof.targets) return; + if (lit.op !== "const" || lit.imms["kind"] !== "atom") return; + const ty = lit.imms["value"] as string; + if (!TYPEOF_IS_TYPES.has(ty)) return; + inst.op = "typeof_is"; + inst.operands.length = 0; + inst.operands.push(tof.operands[0]!); + inst.imms = { type: ty }; + stats.typeof_rewrites++; + changed = true; + }); + return changed; +} + +// --- branch folding + logical_not inversion --------------------------------- + +// truthiness of a value, when provable: consts decide exactly; +// undefined/null are always falsy; objects and functions are always +// truthy (no document.all in this runtime). +function knownTruthiness(v: Inst, tags: Lattice): boolean | undefined { + if (v.op === "const") return !!constPayload(v); + const t = tags[v.id]; + if (t === "undefined" || t === "null") return false; + if (t === "object" || t === "function") return true; + return undefined; +} + +// tags that can never carry a shape header. NB: there is deliberately +// NO has_tag FALSE-folding here — a boxed-repr slot_store's verifier +// proof IS a dominating has_tag=false fact, and folding the branch +// deletes the fact out from under the surviving store (caught by the +// --types lane on every class file). has_shape folds are safe: the +// slot ops that need the fact live in the folded-away fast arm. +const NEVER_SHAPED = new Set(["number", "string", "boolean", "undefined", "null"]); + +function foldBranches(fn: Func, tags: Lattice, stats: OptStats): boolean { + let changed = false; + // to_boolean use counts, for the inversion's locality check + const uses = new Map(); + fn.forEachInst((inst) => { + for (const o of inst.operands) if (o.op === "to_boolean" || o.op === "logical_not") + uses.set(o, (uses.get(o) || 0) + 1); + if (inst.targets) + for (const t of inst.targets) + for (const a of t.args) + if (a && (a.op === "to_boolean" || a.op === "logical_not")) + uses.set(a, (uses.get(a) || 0) + 1); + }); + + for (const b of fn.blocks) { + const term = b.terminator; + if (!term || term.op !== "cond_br") continue; + const cond = term.operands[0]!; + if (cond.op === "to_boolean") { + // invert through logical_not first: branching on !x is + // branching on x with the targets swapped. Sound only when + // this cond_br is the to_boolean's single consumer (the + // rewrite changes its meaning). + let inverted = true; + while (inverted) { + inverted = false; + const src = cond.operands[0]!; + if ( + src.op === "logical_not" && + !src.targets && + uses.get(cond) === 1 && + cond.block === b + ) { + cond.operands[0] = src.operands[0]!; + const t0: Target = term.targets![0]!; + const t1: Target = term.targets![1]!; + term.targets![0] = t1; + term.targets![1] = t0; + // predEdges' targetIndex must track the swap (both + // targets may name the same block — flip each edge + // exactly once) + const targetBlocks = new Set([t0.block, t1.block]); + for (const blk of targetBlocks) { + for (const e of blk.predEdges) { + if (e.inst === term) e.targetIndex = e.targetIndex === 0 ? 1 : 0; + } + } + uses.set(src, (uses.get(src) || 1) - 1); + stats.branches_folded++; + changed = true; + inverted = true; + } + } + const truth = knownTruthiness(cond.operands[0]!, tags); + if (truth !== undefined) { + condBrToBr(fn, b, truth ? 0 : 1); + stats.branches_folded++; + changed = true; + } + } else if (cond.op === "has_shape") { + const t = tags[cond.operands[0]!.id]; + if (t && NEVER_SHAPED.has(t)) { + condBrToBr(fn, b, 1); + stats.branches_folded++; + changed = true; + } + } + } + return changed; +} + +// --- trivial block params --------------------------------------------------- + +// a param fed the same SSA value on every edge (self-edges vacuous) IS +// that value: the value's def dominates every pred's terminator, hence +// the param's block, hence every use of the param. +function pruneTrivialParams(fn: Func, stats: OptStats): boolean { + let changed = false; + for (const b of fn.blocks) { + if (b === fn.entry) continue; // calling convention + for (const p of b.params.slice()) { + if (p.removed || p.isException || p.type !== "any" || p.rawJoin) continue; + if (b.predEdges.length === 0) continue; + const argIdx = b.argIndexOfParam(p); + let v: Inst | null = null; + let ok = true; + for (const e of b.predEdges) { + const arg = e.inst.targets![e.targetIndex]!.args[argIdx]; + if (!arg) { + ok = false; + break; + } + if (arg === p) continue; + if (v === null) v = arg; + else if (v !== arg) { + ok = false; + break; + } + } + if (!ok || v === null) continue; + replaceAllUses(fn, p, v); + b.removeParam(p); + stats.params_pruned++; + changed = true; + } + } + return changed; +} + +// --- lattice-typed low-tier lowering ---------------------------------------- + +const F64_OP: Record = { + add: "f64_add", + sub: "f64_sub", + mul: "f64_mul", + div: "f64_div", +}; + +function insertBefore(fn: Func, anchor: Inst, inst: Inst): Inst { + const b = anchor.block!; + inst.block = b; + b.insts.splice(b.insts.indexOf(anchor), 0, inst); + return inst; +} + +function removeFromBlock(inst: Inst): void { + const b = inst.block!; + const idx = b.insts.indexOf(inst); + if (idx >= 0) b.insts.splice(idx, 1); + inst.block = null; +} + +function latticeLowerArith(fn: Func, tags: Lattice, stats: OptStats): boolean { + let changed = false; + + // use map for the lt/gt consumer-pattern check + const uses = new Map(); + fn.forEachInst((inst) => { + for (const o of inst.operands) { + let l = uses.get(o); + if (!l) uses.set(o, (l = [])); + l.push(inst); + } + if (inst.targets) + for (const t of inst.targets) + for (const a of t.args) + if (a) { + let l = uses.get(a); + if (!l) uses.set(a, (l = [])); + l.push(inst); + } + }); + + const bothNumber = (inst: Inst) => + tags[inst.operands[0]!.id] === "number" && tags[inst.operands[1]!.id] === "number"; + + const worklist: Inst[] = []; + fn.forEachInst((inst) => worklist.push(inst)); + + for (const inst of worklist) { + if (!inst.block) continue; + if (inst.targets && inst.targets.length > 0) continue; + const op = inst.op; + + if (F64_OP[op] !== undefined && bothNumber(inst)) { + const a = inst.operands[0]!; + const b = inst.operands[1]!; + if (a.op === "const" && b.op === "const") continue; // constFold's job + const ua = insertBefore(fn, inst, new Inst(fn, "unbox_f64", [a], {})); + const ub = insertBefore(fn, inst, new Inst(fn, "unbox_f64", [b], {})); + const f = insertBefore(fn, inst, new Inst(fn, F64_OP[op]!, [ua, ub], {})); + const boxed = insertBefore(fn, inst, new Inst(fn, "box_f64", [f], {})); + replaceAllUses(fn, inst, boxed); + removeFromBlock(inst); + stats.lattice_arith++; + changed = true; + continue; + } + + if (op === "unary_plus" && tags[inst.operands[0]!.id] === "number") { + // +x for a number x is x + replaceAllUses(fn, inst, inst.operands[0]!); + removeFromBlock(inst); + stats.lattice_arith++; + changed = true; + continue; + } + + if ((op === "lt" || op === "gt") && bothNumber(inst)) { + // only the whole same-block lt/to_boolean/cond_br chain + // rewrites: f64_lt's i1 must not leak anywhere else + const us = uses.get(inst) || []; + if (us.length !== 1) continue; + const tob = us[0]!; + if (tob.op !== "to_boolean" || tob.block !== inst.block) continue; + const tobUses = uses.get(tob) || []; + if (tobUses.length !== 1) continue; + const cbr = tobUses[0]!; + if (cbr.op !== "cond_br" || cbr.block !== inst.block) continue; + const a = inst.operands[0]!; + const b = inst.operands[1]!; + const ua = insertBefore(fn, inst, new Inst(fn, "unbox_f64", [a], {})); + const ub = insertBefore(fn, inst, new Inst(fn, "unbox_f64", [b], {})); + // a > b is b < a for numbers (NaN compares false either way) + const f = + op === "lt" + ? new Inst(fn, "f64_lt", [ua, ub], {}) + : new Inst(fn, "f64_lt", [ub, ua], {}); + insertBefore(fn, inst, f); + cbr.operands[0] = f; + removeFromBlock(tob); + removeFromBlock(inst); + stats.lattice_arith++; + changed = true; + } + } + return changed; +} + +// --- driver ----------------------------------------------------------------- + +export function cleanupFunction(fn: Func, stats: OptStats): boolean { + let any = false; + for (let round = 0; round < 5; round++) { + const tags = computeLattice(fn); + let changed = false; + if (foldConstants(fn, tags, stats)) changed = true; + if (typeofIsPeephole(fn, stats)) changed = true; + if (foldBranches(fn, tags, stats)) { + sweepUnreachableBlocks(fn); + changed = true; + } + if (pruneTrivialParams(fn, stats)) changed = true; + if (latticeLowerArith(fn, tags, stats)) changed = true; + if (!changed) break; + any = true; + } + return any; +} + +// --- module-slot load CSE --------------------------------------------------- + +export function slotKey(module: string, slot: number): string { + return `${module}#${slot}`; +} + +// the STABLE %self slots: exactly one module_slot_store in the whole +// module, sitting in the toplevel's entry block (which has no +// back-edges and — via the init flag set before the body runs — can +// execute at most once per process). A stable slot's value cannot +// change during any function activation: the toplevel's remaining +// stores only resume after a callee returns, and re-entry is blocked +// by the flag. Export-accessor setters are module_slot_stores too, so +// an externally-writable export can never look stable. +export function computeStableSlots(fns: readonly Func[], toplevelName: string): Set { + const count = new Map(); + const storeIn = new Map(); + for (const fn of fns) { + fn.forEachInst((inst) => { + if (inst.op !== "module_slot_store") return; + const key = slotKey(inst.imms["module"] as string, inst.imms["slot"] as number); + count.set(key, (count.get(key) || 0) + 1); + storeIn.set(key, { fn, inst }); + }); + } + const stable = new Set(); + count.forEach((n, key) => { + if (n !== 1 || !key.startsWith("%self#")) return; + const s = storeIn.get(key)!; + if (s.fn.name !== toplevelName) return; + const entry = s.fn.entry; + if (!entry || s.inst.block !== entry || entry.predEdges.length > 0) return; + stable.add(key); + }); + return stable; +} + +// is `a` before `b`: same block by position, else by dominance +function comesBefore(idom: Map, a: Inst, b: Inst): boolean { + const ba = a.block!; + const bb = b.block!; + if (ba === bb) return ba.insts.indexOf(a) < bb.insts.indexOf(b); + return dominates(idom, ba, bb); +} + +export function cseModuleSlotLoads( + fn: Func, + stableSlots: Set | undefined, + stats: OptStats +): boolean { + let changed = false; + + // the stability arguments below reason about one ACTIVATION: a + // suspendable activation (a desugared generator body — its yields + // lower to generator_* runtime calls) can see the toplevel's + // remaining stores run mid-flight, so it gets no exemptions. + let suspends = false; + fn.forEachInst((inst) => { + if ( + inst.op === "call_runtime" && + typeof inst.imms["name"] === "string" && + (inst.imms["name"] as string).indexOf("generator_") === 0 + ) + suspends = true; + }); + + const isStable = (key: string) => !suspends && !!stableSlots && stableSlots.has(key); + + // (1) block-local availability + store-to-load forwarding + for (const b of fn.blocks) { + const avail = new Map(); + for (const inst of b.insts.slice()) { + if (inst.block !== b) continue; // removed below + if (inst.op === "module_slot_load") { + const key = slotKey(inst.imms["module"] as string, inst.imms["slot"] as number); + const prev = avail.get(key); + if (prev) { + replaceAllUses(fn, inst, prev); + removeFromBlock(inst); + stats.slot_loads_cse++; + changed = true; + } else { + avail.set(key, inst); + } + } else if (inst.op === "module_slot_store") { + const key = slotKey(inst.imms["module"] as string, inst.imms["slot"] as number); + avail.set(key, inst.operands[0]!); + } else if ((opInfo(inst.op).effects & Effect.CALL) !== 0) { + // arbitrary JS may execute this module's stores — only + // stable slots survive (their one store cannot run + // mid-activation; see computeStableSlots) + for (const key of Array.from(avail.keys())) { + if (!isStable(key)) avail.delete(key); + } + } + } + } + + // (2) the stable-slot dominance tier + if (!suspends && stableSlots && stableSlots.size > 0) { + const loadsByKey = new Map(); + const storeByKey = new Map(); + fn.forEachInst((inst) => { + if (inst.op === "module_slot_load") { + const key = slotKey(inst.imms["module"] as string, inst.imms["slot"] as number); + let l = loadsByKey.get(key); + if (!l) loadsByKey.set(key, (l = [])); + l.push(inst); + } else if (inst.op === "module_slot_store") { + const key = slotKey(inst.imms["module"] as string, inst.imms["slot"] as number); + storeByKey.set(key, inst); + } + }); + + let idom: Map | null = null; + let blockOrder: Map | null = null; + const domInfo = () => { + if (!idom) { + const { rpo } = computeRPO(fn); + idom = computeDominators(fn, rpo); + blockOrder = new Map(); + rpo.forEach((b, i) => blockOrder!.set(b, i)); + } + return { idom: idom!, blockOrder: blockOrder! }; + }; + + loadsByKey.forEach((loads, key) => { + if (!isStable(key)) return; + const store = storeByKey.get(key); + if (store && store.block) { + // the module's one store lives in THIS function (so + // this IS the toplevel, per computeStableSlots): loads + // the store comes-before fold to the stored value + const { idom } = domInfo(); + const value = store.operands[0]!; + for (const load of loads) { + if (!load.block) continue; // CSE'd by the block-local tier + if (!comesBefore(idom, store, load)) continue; // pre-init read + replaceAllUses(fn, load, value); + removeFromBlock(load); + stats.slot_loads_cse++; + changed = true; + } + } else { + // store elsewhere (the toplevel init): the slot cannot + // change during this activation — dominated loads fold + // to their dominators + const { idom, blockOrder } = domInfo(); + const live = loads.filter((l) => l.block !== null); + live.sort((a, b) => { + const ba = blockOrder.get(a.block!) ?? 0; + const bb = blockOrder.get(b.block!) ?? 0; + if (ba !== bb) return ba - bb; + return a.block!.insts.indexOf(a) - b.block!.insts.indexOf(b); + }); + const survivors: Inst[] = []; + for (const load of live) { + let folded = false; + for (const p of survivors) { + if (comesBefore(idom, p, load)) { + replaceAllUses(fn, load, p); + removeFromBlock(load); + stats.slot_loads_cse++; + changed = true; + folded = true; + break; + } + } + if (!folded) survivors.push(load); + } + } + }); + } + + return changed; +} diff --git a/lib/eir/devirt.ts b/lib/eir/devirt.ts new file mode 100644 index 00000000..f2e09e27 --- /dev/null +++ b/lib/eir/devirt.ts @@ -0,0 +1,224 @@ +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: + */ + +// direct-call devirtualization beyond the self binding (compiler-P1). +// Lowering only marks direct calls for self-recursion; module +// functions call each other through their %self slots, and function +// expressions through their SSA closure values. When the CALLEE +// IDENTITY of a plain `call` is provable, the call skips closure +// dispatch (_ejs_invoke_closure) and calls the EIR function directly — +// same calling convention, argc/argv passed as before, so callee-side +// defaults/rest/arguments all still work. +// +// Two provable shapes: +// - SSA-visible: the callee operand IS a make_closure in the same +// function. The direct call's env operand is the closure's env. +// - stable-slot: the callee operand is a module_slot_load of a %self +// slot with exactly ONE static store in the module, whose stored +// value is a make_closure, and the load provably observes the +// store: either the store sits in the toplevel entry block with no +// CALL-effect instruction before it (no user code can run before +// the slot is written — cross-function safe, mirroring +// specialize.ts's prefix rule), or the store dominates the load in +// the same function. Cross-function sites can't carry the env +// value, so they additionally require the callee's %env param to +// be entirely unused (typical for module-level functions — their +// free names resolve through module slots, not the environment). +// +// What invoke_closure does that a direct call skips: the IS_FUNCTION +// check (statically true — the value is this closure) and the +// class-constructor TypeError. The latter is why any function whose +// closure might flow into set_constructor_kind_base/derived is +// declined; when that flow isn't enumerable (the marking intrinsic's +// operand is neither a make_closure nor a %self slot load), the pass +// declines the whole module (fail closed). +// +// Runs AFTER specialization (integrate.ts): a devirtualized site no +// longer uses its closure/slot-load as a plain-call callee, which +// would otherwise make specialize.ts's closed-world enumeration +// decline the strictly-better call_typed rewrite. + +import { Module, Func, Inst, Block } from "./ir"; +import { Effect, opInfo } from "./ops"; +import { computeRPO, computeDominators, dominates } from "./verifier"; +import { passes } from "../pass-config"; + +export interface DevirtStats { + // call sites rewritten against an SSA-visible make_closure + ssa_sites: number; + // call sites rewritten through a stable %self slot + slot_sites: number; +} + +function comesBefore(idom: Map, a: Inst, b: Inst): boolean { + const ba = a.block!; + const bb = b.block!; + if (ba === bb) return ba.insts.indexOf(a) < bb.insts.indexOf(b); + return dominates(idom, ba, bb); +} + +export function devirtualizeModule(m: Module, toplevelName: string): DevirtStats { + const stats: DevirtStats = { ssa_sites: 0, slot_sites: 0 }; + if (!passes().devirt) return stats; + + const fnByName = new Map(); + for (const fn of m.functions) fnByName.set(fn.name, fn); + const toplevelFn = fnByName.get(toplevelName); + + // --- class-constructor suspects (fail closed) --------------------------- + // a devirtualized call to a class constructor would skip + // invoke_closure's TypeError; enumerate every closure the marking + // intrinsic could reach and decline those functions. + const ctorSuspect = new Set(); + const suspectSlots = new Set(); + let bailAll = false; + for (const fn of m.functions) { + fn.forEachInst((inst) => { + if (inst.op !== "call_runtime") return; + const name = inst.imms["name"] as string; + if (typeof name !== "string" || name.indexOf("set_constructor_kind") !== 0) return; + const v = inst.operands[0]; + if (!v) return; + if (v.op === "make_closure") ctorSuspect.add(v.imms["fn"] as string); + else if (v.op === "module_slot_load" && v.imms["module"] === "%self") + suspectSlots.add(v.imms["slot"] as number); + else bailAll = true; + }); + } + if (bailAll) return stats; + + // --- %self slot stores -------------------------------------------------- + const selfStores = new Map(); + for (const fn of m.functions) { + fn.forEachInst((inst) => { + if (inst.op !== "module_slot_store" || inst.imms["module"] !== "%self") return; + const slot = inst.imms["slot"] as number; + let l = selfStores.get(slot); + if (!l) selfStores.set(slot, (l = [])); + l.push({ fn, inst }); + }); + } + // anything stored to a ctor-marked slot is suspect too; a + // non-closure store to one means we can't enumerate — fail closed + suspectSlots.forEach((slot) => { + for (const s of selfStores.get(slot) || []) { + if (s.inst.operands[0]!.op === "make_closure") + ctorSuspect.add(s.inst.operands[0]!.imms["fn"] as string); + else bailAll = true; + } + }); + if (bailAll) return stats; + + // --- helpers ------------------------------------------------------------ + const envUnusedCache = new Map(); + const envUnused = (fn: Func): boolean => { + let r = envUnusedCache.get(fn); + if (r !== undefined) return r; + const envParam = fn.entry ? fn.entry.params[0] : undefined; + r = true; + if (envParam) { + fn.forEachInst((inst) => { + for (const o of inst.operands) if (o === envParam) r = false; + if (inst.targets) + for (const t of inst.targets) + for (const a of t.args) if (a === envParam) r = false; + }); + } + envUnusedCache.set(fn, r); + return r; + }; + + // the store sits in the toplevel entry with no CALL-effect + // instruction before it: no user code can observe the slot's + // pre-store state (except a textually-earlier load in that same + // entry block — the documented hoisting-lost read) + const prefixSafeCache = new Map(); + const prefixSafe = (store: Inst, storeFn: Func): boolean => { + let r = prefixSafeCache.get(store); + if (r !== undefined) return r; + r = false; + if (toplevelFn && storeFn === toplevelFn && store.block === toplevelFn.entry) { + r = true; + for (const inst of toplevelFn.entry!.insts) { + if (inst === store) break; + if ((opInfo(inst.op).effects & Effect.CALL) !== 0) { + r = false; + break; + } + } + } + prefixSafeCache.set(store, r); + return r; + }; + + const idoms = new Map>(); + const idomOf = (fn: Func): Map => { + let d = idoms.get(fn); + if (!d) { + const { rpo } = computeRPO(fn); + idoms.set(fn, (d = computeDominators(fn, rpo))); + } + return d; + }; + + // --- the rewrite -------------------------------------------------------- + // candidates first (rewriting inserts instructions, which must not + // happen under forEachInst's live iteration) + for (const fn of m.functions) { + const ssa: { call: Inst; closure: Inst; name: string }[] = []; + const slot: { call: Inst; name: string }[] = []; + fn.forEachInst((inst) => { + if (inst.op !== "call" || inst.imms["direct"]) return; + const callee = inst.operands[0]!; + + if (callee.op === "make_closure") { + const name = callee.imms["fn"] as string; + if (ctorSuspect.has(name) || !fnByName.has(name)) return; + ssa.push({ call: inst, closure: callee, name }); + return; + } + + if (callee.op === "module_slot_load" && callee.imms["module"] === "%self") { + const slotNum = callee.imms["slot"] as number; + const stores = selfStores.get(slotNum) || []; + if (stores.length !== 1) return; + const { fn: storeFn, inst: store } = stores[0]!; + const closure = store.operands[0]!; + if (closure.op !== "make_closure") return; + const name = closure.imms["fn"] as string; + const target = fnByName.get(name); + if (!target || ctorSuspect.has(name)) return; + if (!envUnused(target)) return; + // the load must provably observe the store + const load = callee; + let orderOk: boolean; + if (prefixSafe(store, storeFn)) { + orderOk = !( + load.block === store.block && + store.block!.insts.indexOf(load) < store.block!.insts.indexOf(store) + ); + } else { + orderOk = storeFn === fn && comesBefore(idomOf(fn), store, load); + } + if (!orderOk) return; + slot.push({ call: inst, name }); + } + }); + for (const c of ssa) { + c.call.imms["direct"] = c.name; + c.call.operands[0] = c.closure.operands[0]!; + stats.ssa_sites++; + } + for (const c of slot) { + const env = new Inst(fn, "const", [], { kind: "undefined" }); + const b = c.call.block!; + env.block = b; + b.insts.splice(b.insts.indexOf(c.call), 0, env); + c.call.imms["direct"] = c.name; + c.call.operands[0] = env; + stats.slot_sites++; + } + } + return stats; +} diff --git a/lib/eir/emit.ts b/lib/eir/emit.ts new file mode 100644 index 00000000..20ea2608 --- /dev/null +++ b/lib/eir/emit.ts @@ -0,0 +1,1437 @@ +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: + */ + +// EIR -> LLVM emission. +// +// Every EIR value becomes an LLVM SSA value; block arguments become phis; +// invoke-style instructions (normal/unwind targets) become llvm invokes +// landing in the catch block's landingpad. The only allocas are the arg +// scratch area and the &this slot that the runtime calling convention +// requires -- locals never touch memory, and mem2reg has nothing to do. +// +// The emitter borrows the active LLVMIRVisitor's infrastructure (llvm +// module, abi, runtime interface, atom/string-literal machinery) through +// the VisitorSurface interface below. + +import * as llvm from "@llvm"; +import * as types from "../types"; +import * as consts from "../consts"; +import type { ABI } from "../abi"; +import type { RuntimeInterface } from "../runtime"; +import type { Module as EIRModule, Func, Block, Inst, Target } from "./ir"; +import { passes } from "../pass-config"; +import { computeSpilledValues } from "./liveness"; + +const ir = llvm.IRBuilder; + +// the slice of LLVMIRVisitor the emitter uses (compiler.ts implements it) +export interface VisitorSurface { + currentFunction: llvm.EjsFunction | null; + ejs_runtime: RuntimeInterface; + ejs_binops: Record; + ejs_globals: Record; + import_module_globals: Map; + this_module_global: llvm.GlobalVariable; + getAtom(str: string): llvm.Value; + createEjsValueLoad(value: llvm.Value, name: string): llvm.Value; + emitEjsvalFromPtr(ptr: llvm.Value, prefix: string): llvm.Value; + isNumber(val: llvm.Value): llvm.Value; + // the low tier's NaN-box transfers (implemented beside isNumber in + // compiler.ts so all target-layout knowledge stays in one place) + unboxDouble(val: llvm.Value): llvm.Value; + boxDouble(dbl: llvm.Value): llvm.Value; + // shape-guard NaN-box tests (beside isNumber for the same reason): the object + // tag test, the payload->EJSObject* reinterpretation (valid only under + // a passed isObject), and the module's interned shape-index global + isObject(val: llvm.Value): llvm.Value; + objectPointer(val: llvm.Value): llvm.Value; + // the inline half of the write barrier — "is this + // value's payload in the nursery range" (layout knowledge lives in + // compiler.ts with the other NaN-box tests) + emitYoungCheck(val: llvm.Value): llvm.Value; + // i1: the runtime's accessor epoch is still zero (one global load + + // compare; the global lives beside the other runtime seams) + emitAccessorEpochCheck(): llvm.Value; + // inline nursery bump allocation for closure envs + emitEnvAllocInline(n: number, slowCall: () => llvm.Value): llvm.Value; + // the gc-frame record (precise relocatable JS roots) and + // inline env slot addressing (all layout knowledge in compiler.ts) + emitGCFrameLink(frame: llvm.Value, nslots: number, undef: llvm.Value): void; + emitGCFrameUnlink(frame: llvm.Value): void; + emitGCFrameRelink(frame: llvm.Value): void; + gcFrameSlotPtr(frame: llvm.Value, i: number): llvm.Value; + emitEnvSlotRef(env: llvm.Value, slot: number): llvm.Value; + moduleShapeGlobal( + key: string, + fields: { name: string; repr: string }[] + ): llvm.GlobalVariable; + loadBoolEjsValue(n: boolean): llvm.Value; + loadDoubleEjsValue(n: number): llvm.Value; + loadNullEjsValue(): llvm.Value; + loadUndefinedEjsValue(): llvm.Value; +} + +// EIR opcode -> the operator key used by runtime.ts's binop interface +const binop_for_op: Record = { + add: "+", + sub: "-", + mul: "*", + div: "/", + mod: "%", + lt: "<", + le: "<=", + gt: ">", + ge: ">=", + loose_eq: "==", + loose_neq: "!=", + strict_eq: "===", + strict_neq: "!==", + bitand: "&", + bitor: "|", + bitxor: "^", + shl: "<<", + shr: ">>", + ushr: ">>>", + instanceof: "instanceof", + in: "in", +}; + +const unop_for_op: Record = { + logical_not: "!", + neg: "-", + unary_plus: "+", + bitnot: "~", + typeof: "typeof", +}; + +let mangle_gen = 0; + +// reachable blocks of `fn` in reverse postorder (entry first). iterative +// DFS: block counts are small, but the self-hosted stack isn't deep. +function rpoBlocks(fn: Func): Block[] { + const entry = fn.entry!; + const visited = new Set([entry]); + const post: Block[] = []; + const stack = [{ block: entry, next: 0 }]; + while (stack.length > 0) { + const frame = stack[stack.length - 1]!; + const last = frame.block.insts[frame.block.insts.length - 1]; + const targets = (last && last.targets) || []; + if (frame.next < targets.length) { + const succ = targets[frame.next++]!.block; + if (!visited.has(succ)) { + visited.add(succ); + stack.push({ block: succ, next: 0 }); + } + } else { + post.push(frame.block); + stack.pop(); + } + } + post.reverse(); + return post; +} + +export class EIREmitter { + // the active LLVMIRVisitor: its module, abi, runtime/binop + // interfaces, getAtom, and globals + v: VisitorSurface; + abi: ABI; + module: llvm.Module; + // per-module state + llvm_fns!: Map; + eirModule!: EIRModule; + // per-function state (reset in emitFunction) + eirFn!: Func; + llvmFn!: llvm.EjsFunction; + values!: Map; + blocks!: Map; + phis!: Map; + fn_argc!: llvm.Value; + fn_args_ptr!: llvm.Value; + fn_this_ptr!: llvm.Value; + fn_new_target!: llvm.Value; + scratch: llvm.AllocaInst | null = null; + // the slot-array env loaded by the most recent slotRef — + // shaped stores must remember the ENV (the storage owner), not the + // object whose Scan only holds the env reference + scratch_type: llvm.Type | null = null; + this_slot!: llvm.AllocaInst; + // values live across a safepoint are DEMOTED to + // gc-frame slots — stored once where they're defined, LOADED at + // every use (val() intercepts). Every load is dominated by its + // def's store; LLVM CSEs redundant loads between safepoints but + // cannot forward across one (the frame escapes via the chain), so a + // collector rewrite is always observed. + gc_frame: llvm.AllocaInst | null = null; + gc_frame_slots: Map | null = null; + + constructor(visitor: VisitorSurface & { abi: ABI; module: llvm.Module }) { + this.v = visitor; + this.abi = visitor.abi; + this.module = visitor.module; + } + + // declare + define every function in an EIR module; returns a Map of + // eir function name -> llvm.Function + emitModule(eirModule: EIRModule): Map { + this.eirModule = eirModule; + let saved_insert = ir.getInsertBlock(); + + let fns = new Map(); + for (let fn of eirModule.functions) { + if (fns.has(fn.name)) + throw new Error(`EIR emit: duplicate function name '${fn.name}' in module`); + let llvm_name = `_ejs_eir_${fn.name.replace(/[^A-Za-z0-9_]/g, "_")}_${mangle_gen++}`; + let llvm_fn; + if (fn.sig) { + // specialized clone: a native unboxed signature + // — (double...) -> double — instead of the runtime's boxed + // (env, this*, argc, argv*, newTarget) convention. The + // specialization post-checks guarantee the clone touches + // neither env nor `this`, so NEITHER gets an argument slot + // (the EIR-level %env operand of call_typed is simply not + // emitted) — one less register at every out-of-line call + // site, and LLVM's O2 pipeline demonstrably does not + // dead-arg-eliminate it for us. Internal-linkage, + // direct-call-only (call_typed), so no takes_builtins and + // no closure-dispatch interop; this is what finally lets + // LLVM inline and scalar-optimize through the call. + const param_types = fn.sig.formals.map((f) => + f === "f64" ? types.Double : types.EjsValue + ); + const ret_type = fn.sig.result === "f64" ? types.Double : types.EjsValue; + llvm_fn = this.abi.createFunction(this.module, llvm_name, ret_type, param_types); + } else { + llvm_fn = types.takes_builtins( + this.abi.createFunction( + this.module, + llvm_name, + this.abi.ejs_return_type, + this.abi.ejs_params.map((p) => p.llvm_type) + ) + ); + } + llvm_fn.setInternalLinkage(); + fns.set(fn.name, llvm_fn); + } + this.llvm_fns = fns; + + for (let fn of eirModule.functions) this.emitFunction(fn, fns.get(fn.name)!); + + if (saved_insert) ir.setInsertPoint(saved_insert); + return fns; + } + + emitFunction(eirFn: Func, llvmFn: llvm.EjsFunction): llvm.EjsFunction { + this.eirFn = eirFn; + this.llvmFn = llvmFn; + this.values = new Map(); // eir Inst -> llvm value + this.blocks = new Map(); // eir Block -> llvm BasicBlock + this.phis = new Map(); // eir blockparam Inst -> llvm phi + + // legacy machinery (getAtom / literal loads) expects these on the + // visitor's current function + let saved_function = this.v.currentFunction; + this.v.currentFunction = llvmFn; + + let entry_bb = new llvm.BasicBlock("entry", llvmFn); + ir.setInsertPoint(entry_bb); + llvmFn.entry_bb = entry_bb; // literal allocas / legacy helpers want this + llvmFn.literalAllocas = Object.create(null); + + const args = llvmFn.args; + // specialized clones have no env/this*/argc/argv*/newTarget — their + // llvm args are the formals alone; the specialization pass + // guarantees no op that needs the frame values survives (frame + // ops, env/this uses, and generic returns all discard a clone) + const env = eirFn.sig ? undefined! : args[0]!; + const this_ptr = eirFn.sig ? undefined! : args[1]!; + const argc = eirFn.sig ? undefined! : args[2]!; + const args_ptr = eirFn.sig ? undefined! : args[3]!; + // rest_args / args_obj / construct_super / new_target need the raw + // calling-convention values + this.fn_argc = argc; + this.fn_args_ptr = args_ptr; + this.fn_this_ptr = this_ptr; + this.fn_new_target = eirFn.sig ? undefined! : args[4]!; + + // scratch space for outgoing call arguments, and a slot for passing + // &this to the runtime's calling convention + let max_args = this.maxOutgoingArgs(eirFn); + this.scratch = null; + this.scratch_type = null; + if (max_args > 0) { + this.scratch_type = llvm.ArrayType.get(types.EjsValue, max_args); + this.scratch = ir.createAlloca(this.scratch_type, "args_scratch"); + this.scratch.setAlignment(8); + } + this.this_slot = ir.createAlloca(types.EjsValue, "this_slot"); + this.this_slot.setAlignment(8); + + // values live across safepoints get frame slots + this.gc_frame = null; + this.gc_frame_slots = null; + if (passes().gcFrames) { + const spilled = computeSpilledValues(eirFn); + if (spilled) { + const slots = new Map(); + let i = 0; + for (const v of spilled) slots.set(v, i++); + this.gc_frame_slots = slots; + this.gc_frame = ir.createAlloca( + llvm.ArrayType.get(types.Int64, 2 + slots.size), + "gc_frame" + ); + this.gc_frame.setAlignment(8); + } + } + + // emit blocks in reverse postorder: a def's block always precedes + // its uses' blocks (dominators come first in any RPO), so the + // values map is filled before it's read. block *creation* order in + // the lowerer doesn't have that property (e.g. switch bodies are + // created before their test chain). unreachable blocks are dropped + // entirely — nothing branches to them, and their phis would be + // invalid (zero incoming edges). + let order = rpoBlocks(eirFn); + + // create llvm blocks for every reachable eir block, and phis for + // their params + for (let b of order) { + let bb = new llvm.BasicBlock(b.name, llvmFn); + this.blocks.set(b, bb); + } + for (let b of order) { + if (b === eirFn.entry) continue; + ir.setInsertPoint(this.blocks.get(b)!); + for (let p of b.params) { + if (p.isException) continue; // materialized by the landingpad below + // rawJoin params (the raw-join pass) carry raw doubles; + // everything else is an EjsValue phi (the P2 boxed rule) + let phi_type = p.type === "f64" ? types.Double : types.EjsValue; + let phi = ir.createPhi(phi_type, b.predEdges.length, `p_${p.id}`); + this.phis.set(p, phi); + this.values.set(p, phi); + } + if (b.isCatch) this.emitCatchPrologue(b); + } + + // entry prologue: bind the eir entry params. this happens in a + // separate block because entry_bb must stay terminator-free until + // the very end: the legacy cached-literal helpers append their + // initializing stores to it whenever a literal is first used. + let prologue_bb = new llvm.BasicBlock("prologue", llvmFn); + ir.setInsertPoint(prologue_bb); + // link the frame before anything can allocate; slots + // start as undefined so a pre-def walk sees valid ejsvals + if (this.gc_frame) + this.v.emitGCFrameLink(this.gc_frame, this.gc_frame_slots!.size, this.undef()); + const entry_params = eirFn.entry!.params; + // params[0] = %env, params[1] = %this, rest are JS formals + if (eirFn.sig) { + // typed convention: formals arrive directly (raw doubles for + // f64 formals) at llvm args [0..]. %env and %this have NO + // argument slots and are required-unused — left unbound, so a + // stray use fails loudly at val() + for (let i = 2; i < entry_params.length; i++) + this.values.set(entry_params[i]!, args[i - 2]!); + } else { + if (entry_params.length > 0) this.values.set(entry_params[0]!, env); + if (entry_params.length > 1) { + let this_val = ir.createLoad(types.EjsValue, this_ptr, "this"); + this.values.set(entry_params[1]!, this_val); + } + for (let i = 2; i < entry_params.length; i++) + this.values.set(entry_params[i]!, this.emitArgLoad(argc, args_ptr, i - 2)); + } + // demote slotted entry params (their canonical home is + // the frame slot from here on; val() loads it per use) + if (this.gc_frame_slots) + for (const p of entry_params) this.demoteToSlot(p); + // remember where the prologue ended; the branch into the eir entry + // block is emitted *after* the body, because the legacy cached- + // literal helpers append their initializing stores to the end of + // whatever block is "entry" at the time they're first used. + let prologue_end = ir.getInsertBlock(); + + // emit every block's instructions + for (let b of order) { + ir.setInsertPoint(this.blocks.get(b)!); + // slotted block params store to their frame slot at + // block entry (after the phis, which the block-creation pass + // already registered) + if (this.gc_frame_slots && b !== eirFn.entry) + for (const p of b.params) this.demoteToSlot(p); + for (let inst of b.insts) { + this.emitInst(inst); + // a slotted def's store follows immediately + // (slotted values are never targets-carrying, so the + // block is not yet terminated here) + if (this.gc_frame_slots && this.gc_frame_slots.has(inst)) + this.demoteToSlot(inst); + } + } + + ir.setInsertPoint(prologue_end); + ir.createBr(this.blocks.get(eirFn.entry!)!); + ir.setInsertPoint(entry_bb); + ir.createBr(prologue_bb); + + this.v.currentFunction = saved_function; + return llvmFn; + } + + // args[i] if i < argc, else undefined -- guarded load with a phi join + emitArgLoad(argc: llvm.Value, args_ptr: llvm.Value, i: number): llvm.Value { + let load_bb = new llvm.BasicBlock(`arg${i}_load`, this.llvmFn); + let join_bb = new llvm.BasicBlock(`arg${i}_join`, this.llvmFn); + const from_bb = ir.getInsertBlock()!; + + // materialize the fallback in the predecessor so it dominates the phi + let undef_val = this.undef(); + let cmp = ir.createICmpUGt(argc, consts.int32(i), `has_arg${i}`); + ir.createCondBr(cmp, load_bb, join_bb); + + ir.setInsertPoint(load_bb); + let gep = ir.createGetElementPointer(types.EjsValue, args_ptr, [consts.int64(i)], "argp"); + let loaded = ir.createLoad(types.EjsValue, gep, `arg${i}`); + ir.createBr(join_bb); + + ir.setInsertPoint(join_bb); + let phi = ir.createPhi(types.EjsValue, 2, `arg${i}v`); + phi.addIncoming(loaded, load_bb); + phi.addIncoming(undef_val, from_bb); + return phi; + } + + emitCatchPrologue(eirBlock: Block): void { + // landingpad; extract the exception; begin/end catch to fetch the + // thrown ejsval. end_catch releases the C++ exception object; the + // value itself is safe (conservatively scanned like any other). + let caught = ir.createLandingPad(types.EjsLandingPad, 1, "caught"); + caught.addClause( + ir.createPointerCast(this.v.ejs_runtime.exception_typeinfo, types.Int8Pointer, "") + ); + caught.setCleanup(true); + if (!this.llvmFn.hasPersonality()) + this.llvmFn.setPersonality( + ir.createPointerCast(this.v.ejs_runtime.personality, types.Int8Pointer, "personality") + ); + + let exc = ir.createExtractValue(caught, 0, "exc"); + let val = this.call(this.v.ejs_runtime.begin_catch, [exc], "caughtval"); + this.call(this.v.ejs_runtime.end_catch, [], ""); + + const exc_param = eirBlock.params[0]!; + this.values.set(exc_param, val); + + // the unwind discarded every callee frame below this one — + // re-link our record as the chain head + if (this.gc_frame) this.v.emitGCFrameRelink(this.gc_frame); + } + + // if `v` has a frame slot, store its current llvm value there + // (its canonical home; val() loads it per use from now on) + demoteToSlot(v: Inst): void { + const slot = this.gc_frame_slots ? this.gc_frame_slots.get(v) : undefined; + if (slot === undefined) return; + const cur = this.values.get(v); + if (cur === undefined) return; // unbound (e.g. clone %env/%this) + ir.createStore(cur, this.v.gcFrameSlotPtr(this.gc_frame!, slot)); + } + + maxOutgoingArgs(eirFn: Func): number { + let max = 0; + eirFn.forEachInst((inst) => { + if (inst.op === "call") max = Math.max(max, inst.operands.length - 2); + else if (inst.op === "construct" || inst.op === "construct_super") + max = Math.max(max, inst.operands.length - 1); + else if (inst.op === "construct_super_apply" || inst.op === "construct_apply") + max = Math.max(max, 1); + else if (inst.op === "make_array" || inst.op === "array_from_spread") + max = Math.max(max, inst.operands.length); + // names + values, spilled contiguously (see the emit case) + else if (inst.op === "make_object_shaped") + max = Math.max(max, inst.operands.length * 2); + else if (inst.op === "fill_object_shaped") + max = Math.max(max, (inst.operands.length - 1) * 2); + else if (inst.op === "template_callsite") + max = Math.max( + max, + (inst.imms["cooked"] as readonly string[]).length, + (inst.imms["raw"] as readonly string[]).length + ); + }); + return max; + } + + // --- helpers ------------------------------------------------------------------- + + val(operand: Inst | null | undefined): llvm.Value { + // a slotted value's canonical home is its gc-frame slot — + // load per use, so a post-safepoint use observes any collector + // rewrite. Loads between safepoints CSE under LLVM; loads + // across one cannot (the frame escapes via the chain). + if (operand && this.gc_frame_slots) { + const slot = this.gc_frame_slots.get(operand); + if (slot !== undefined) + return ir.createLoad( + types.EjsValue, + this.v.gcFrameSlotPtr(this.gc_frame!, slot), + `gcf_v${operand.id}` + ); + } + const v = operand ? this.values.get(operand) : undefined; + if (v === undefined) + throw new Error( + `EIR emit: no llvm value for %v${operand ? operand.id : ""} (${operand ? operand.op : "?"})` + ); + return v; + } + + undef(): llvm.Value { + return this.v.loadUndefinedEjsValue(); + } + + call(callee: llvm.EjsFunction, argv: llvm.Value[], name?: string): llvm.Value { + return this.abi.createCall(this.llvmFn, callee.type, callee, argv, name || ""); + } + + // the emitted generational write barrier (object- + // remembering). Inline: one range check on the stored VALUE; slow: + // _ejs_gc_remember_val(owner, value) marks the owner dirty. With + // the nursery disabled the bounds are zero and the branch is never + // taken. + emitStoreBarrier(owner: llvm.Value, v: llvm.Value): void { + const rt = this.v.ejs_runtime; + const young = this.v.emitYoungCheck(v); + const bar_bb = new llvm.BasicBlock("wb_slow", this.llvmFn); + const cont_bb = new llvm.BasicBlock("wb_cont", this.llvmFn); + ir.createCondBr(young, bar_bb, cont_bb); + ir.setInsertPoint(bar_bb); + this.call(rt.gc_write_barrier, [owner, v]); + ir.createBr(cont_bb); + ir.setInsertPoint(cont_bb); + } + + // THE slot-addressing seam. A shaped object's + // property storage is a closureenv slot array hanging off the + // map/slots union word; when the GC work moves slots inline, + // only this method changes (the ops carry slot indices, not + // addresses). Only valid downstream of a passed has_shape on `objval` + // for a shape with more than `slot` fields — which the EIR verifier + // enforces — so the union word is a non-null slot-array ejsval here. + slotRef(objval: llvm.Value, slot: number): llvm.Value { + const objptr = this.v.objectPointer(objval); + // field 4 of types.EjsObject is the map/slots union word; load it + // as an ejsval (the slot-array reference) + const union_ptr = ir.createInBoundsGetElementPointer( + types.EjsObject, + objptr, + [consts.int64(0), consts.int32(4)], + "slots_union_ptr" + ); + const slots_ptr = ir.createBitCast( + union_ptr, + types.EjsValue.pointerTo(), + "slots_ejsval_ptr" + ); + const slotsval = ir.createLoad(types.EjsValue, slots_ptr, "slots_ejsval"); + // payload-mask the closureenv ejsval to its EJSClosureEnv* + const envptr = ir.createPointerCast( + this.v.objectPointer(slotsval), + types.EjsClosureEnv.pointerTo(), + "slots_env" + ); + // field 4 of types.EjsClosureEnv is the trailing slots array; the + // GEP is deliberately non-inbounds (the array is declared [1 x + // ejsval], the moduleSlotRef precedent for trailing arrays) + return ir.createGetElementPointer( + types.EjsClosureEnv, + envptr, + [consts.int64(0), consts.int32(4), consts.int64(slot)], + "slot_ref" + ); + } + + // same shape as the legacy opencoded module slot access: a non-inbounds + // GEP into the module global (see handleModuleSlotRef in compiler.js). + // "%self" refers to the module being compiled. + moduleSlotRef(moduleString: string, slot: number): llvm.Value { + let module_global; + if (moduleString === "%self") module_global = this.v.this_module_global; + else module_global = this.v.import_module_globals.get(moduleString); + if (!module_global) + throw new Error(`EIR emit: no module global for '${moduleString}'`); + let mg = ir.createPointerCast(module_global, types.EjsModule.pointerTo(), ""); + return ir.createGetElementPointer( + types.EjsModule, + mg, + [consts.int64(0), consts.int32(3), consts.int64(slot)], + "slot_ref" + ); + } + + // spill values into the scratch area, returning an EjsValue* to its start + spillArgs(values: llvm.Value[]): llvm.Value { + for (let i = 0; i < values.length; i++) { + const gep = ir.createGetElementPointer( + this.scratch_type!, + this.scratch!, + [consts.int32(0), consts.int64(i)], + `sp${i}` + ); + ir.createStore(values[i]!, gep); + } + return ir.createGetElementPointer( + this.scratch_type!, + this.scratch!, + [consts.int32(0), consts.int64(0)], + "spargs" + ); + } + + // emit a call to `callee` that respects this instruction's normal/unwind + // targets (invoke) or is a plain call + emitCallLike(inst: Inst, callee: llvm.EjsFunction, argv: llvm.Value[], name?: string): llvm.Value { + if (inst.targets && inst.targets.length > 0) { + let normal = null; + let unwind = null; + for (let t of inst.targets) { + if (t.kind === "unwind") unwind = t; + else normal = t; + } + this.addEdgeIncomings(inst, unwind); + this.addEdgeIncomings(inst, normal); + const normal_bb = this.blocks.get(normal!.block)!; + const unwind_bb = this.blocks.get(unwind!.block)!; + let rv = this.abi.createInvoke( + this.llvmFn, + callee.type, + callee, + argv, + normal_bb, + unwind_bb, + name || "" + ); + this.values.set(inst, rv); + return rv; + } + let rv = this.call(callee, argv, name); + this.values.set(inst, rv); + return rv; + } + + // fill in phi incomings for the arguments this edge passes + addEdgeIncomings(inst: Inst, target: Target | null | undefined): void { + if (!target) return; + const src_bb = ir.getInsertBlock()!; + let params = target.block.params; + let arg_base = target.block.isCatch ? 1 : 0; + for (let i = 0; i < target.args.length; i++) { + const param = params[arg_base + i]!; + const phi = this.phis.get(param); + if (!phi) throw new Error("EIR emit: edge argument for missing phi"); + phi.addIncoming(this.val(target.args[i]), src_bb); + } + } + + // --- instruction emission ----------------------------------------------------------- + + emitInst(inst: Inst): llvm.Value | void { + let rt = this.v.ejs_runtime; + + switch (inst.op) { + case "const": { + let v; + switch ((inst.imms["kind"] as string)) { + case "number": + v = this.v.loadDoubleEjsValue(inst.imms["value"] as number); + break; + case "atom": + v = this.v.getAtom(String(inst.imms.value)); + break; + case "boolean": + v = this.v.loadBoolEjsValue(inst.imms["value"] as boolean); + break; + case "undefined": + v = this.undef(); + break; + case "null": + v = this.v.loadNullEjsValue(); + break; + default: + throw new Error(`EIR emit: const kind ${(inst.imms["kind"] as string)}`); + } + this.values.set(inst, v); + return; + } + + case "to_boolean": { + // produce an i1 for cond_br; only ever consumed by cond_br + let truthy = this.call(rt.truthy, [this.val(inst.operands[0])], "truthy"); + let b = ir.createICmpEq(truthy, consts.True(), "tobool"); + this.values.set(inst, b); + return; + } + + case "typeof_is": { + // the single-tag test cleanup.ts rewrites + // `typeof x === "T"` into; boxed boolean result via the + // runtime's typeof_is_ entries + const t = String(inst.imms["type"]); + const callee = (rt as unknown as Record)[ + `typeof_is_${t}` + ]; + if (!callee) throw new Error(`EIR emit: no typeof_is runtime entry for '${t}'`); + return this.emitCallLike(inst, callee, [this.val(inst.operands[0])], "typeofis"); + } + + // --- the typed low tier --------------------------- + // has_tag/unbox/box mirror LLVMIRVisitor's NaN-boxing helpers; + // the f64_* ops are plain LLVM float arithmetic. has_tag and + // f64_lt produce machine i1 (consumed by cond_br, like + // to_boolean); unbox produces a raw double; box re-enters the + // boxed world. + case "has_tag": { + const tag = inst.imms["tag"]; + if (tag !== "number") + throw new Error(`EIR emit: has_tag tag '${String(tag)}' is not supported`); + this.values.set(inst, this.v.isNumber(this.val(inst.operands[0]))); + return; + } + + // --- shapes ------------------------------- + // has_shape folds the NaN-box object check into the header + // shape-index compare, the way isNumber backs has_tag: a + // non-object is simply false. The shape-index global holds + // EJS_SHAPE_NOMATCH until module init interns the real index + // (and forever, under EJS_SHAPES=off) — an index no object + // header can carry, so the guard is false rather than wrong. + case "has_shape": { + const key = String(inst.imms["shape"]); + const fields = this.eirModule.shapes.get(key); + if (!fields) + throw new Error(`EIR emit: has_shape names unknown module shape '${key}'`); + const g = this.v.moduleShapeGlobal(key, fields); + const val = this.val(inst.operands[0]); + + const check_bb = new llvm.BasicBlock("shape_check", this.llvmFn); + const merge_bb = new llvm.BasicBlock("shape_merge", this.llvmFn); + const from_bb = ir.getInsertBlock()!; + ir.createCondBr(this.v.isObject(val), check_bb, merge_bb); + + ir.setInsertPoint(check_bb); + const objptr = this.v.objectPointer(val); + // GCObjectHeader is two i32 halves in types.EjsObject; the + // shape index is the low 24 bits of the high half + const hdr_hi_ptr = ir.createInBoundsGetElementPointer( + types.EjsObject, + objptr, + [consts.int64(0), consts.int32(1)], + "hdr_hi_ptr" + ); + const hdr_hi = ir.createLoad(types.Int32, hdr_hi_ptr, "hdr_hi"); + const shape_idx = ir.createAnd(hdr_hi, consts.int32(0xffffff), "shape_idx"); + const want = ir.createLoad(types.Int32, g, "shape_want"); + const eq = ir.createICmpEq(shape_idx, want, "shape_eq"); + ir.createBr(merge_bb); + + ir.setInsertPoint(merge_bb); + const phi = ir.createPhi(types.Int1, 2, "has_shape"); + phi.addIncoming(eq, check_bb); + phi.addIncoming(consts.int1(0), from_bb); + this.values.set(inst, phi); + return; + } + // typed slots: an f64-repr slot is accessed as a raw + // double — same address, same 8 bytes (the NaN-box stores + // doubles raw), just loaded/stored as the machine type the + // guard's repr proof licenses. + case "slot_load": { + const ref = this.slotRef(this.val(inst.operands[0]), inst.imms["slot"] as number); + if (inst.imms["repr"] === "f64") { + const dref = ir.createBitCast(ref, types.Double.pointerTo(), "slot_f64_ptr"); + this.values.set(inst, ir.createLoad(types.Double, dref, "slot_f64")); + } else { + this.values.set(inst, ir.createLoad(types.EjsValue, ref, "slot_val")); + } + return; + } + case "slot_store": { + const objval = this.val(inst.operands[0]); + const ref = this.slotRef(objval, inst.imms["slot"] as number); + if (inst.imms["repr"] === "f64") { + // raw doubles are not references: no barrier + const dref = ir.createBitCast(ref, types.Double.pointerTo(), "slot_f64_ptr"); + ir.createStore(this.val(inst.operands[1]), dref); + } else { + ir.createStore(this.val(inst.operands[1]), ref); + // the barrier owner is the wrapper OBJECT (gc-P5): + // its Scan walks the slot values directly, and + // embedded storage is not a cell of its own + this.emitStoreBarrier(objval, this.val(inst.operands[1])); + } + this.values.set(inst, this.val(inst.operands[1])); + return; + } + // born with their shape: spill the field + // names (atom loads) and initial values contiguously into the + // scratch area — names at [0..n), values at [n..2n) — and make + // one runtime call. The runtime re-derives the true shape from + // the actual values and falls back to sequential generic sets + // whenever the shaped fast path doesn't apply, so no shape + // global is consulted here (unlike has_shape). + case "make_object_shaped": + case "fill_object_shaped": { + const key = String(inst.imms["shape"]); + const fields = this.eirModule.shapes.get(key); + if (!fields) + throw new Error(`EIR emit: ${inst.op} names unknown module shape '${key}'`); + const isFill = inst.op === "fill_object_shaped"; + const vals = inst.operands.slice(isFill ? 1 : 0).map((o) => this.val(o)); + const names = fields.map((f) => this.v.getAtom(f.name)); + const base = this.spillArgs([...names, ...vals]); + const vbase = ir.createGetElementPointer( + this.scratch_type!, + this.scratch!, + [consts.int32(0), consts.int64(fields.length)], + "shaped_vals" + ); + const argv = isFill + ? [this.val(inst.operands[0]), consts.int32(fields.length), base, vbase] + : [consts.int32(fields.length), base, vbase]; + this.emitCallLike( + inst, + isFill ? rt.object_fill_shaped : rt.object_new_shaped, + argv, + isFill ? "fillshaped" : "newshaped" + ); + return; + } + case "epoch_check": + this.values.set(inst, this.v.emitAccessorEpochCheck()); + return; + case "unbox_f64": + this.values.set(inst, this.v.unboxDouble(this.val(inst.operands[0]))); + return; + case "f64_const": + this.values.set(inst, llvm.ConstantFP.getDouble(inst.imms["value"] as number)); + return; + case "box_f64": + this.values.set(inst, this.v.boxDouble(this.val(inst.operands[0]))); + return; + case "f64_add": + this.values.set( + inst, + ir.createFAdd(this.val(inst.operands[0]), this.val(inst.operands[1]), "f64_add") + ); + return; + case "f64_sub": + this.values.set( + inst, + ir.createFSub(this.val(inst.operands[0]), this.val(inst.operands[1]), "f64_sub") + ); + return; + case "f64_mul": + this.values.set( + inst, + ir.createFMul(this.val(inst.operands[0]), this.val(inst.operands[1]), "f64_mul") + ); + return; + case "f64_div": + this.values.set( + inst, + ir.createFDiv(this.val(inst.operands[0]), this.val(inst.operands[1]), "f64_div") + ); + return; + case "f64_lt": + this.values.set( + inst, + ir.createFCmpOLT(this.val(inst.operands[0]), this.val(inst.operands[1]), "f64_lt") + ); + return; + + case "get_prop": { + let callee = rt.object_getprop; + return this.emitCallLike( + inst, + callee, + [this.val(inst.operands[0]), this.val(inst.operands[1])], + "getprop" + ); + } + case "get_prop_atom": { + let key = this.v.getAtom(String(inst.imms["atom"])); + return this.emitCallLike( + inst, + rt.object_getprop, + [this.val(inst.operands[0]), key], + "getprop" + ); + } + case "set_prop": { + return this.emitCallLike( + inst, + rt.object_setprop, + [ + this.val(inst.operands[0]), + this.val(inst.operands[1]), + this.val(inst.operands[2]), + ], + "setprop" + ); + } + case "set_prop_atom": { + let key = this.v.getAtom(String(inst.imms["atom"])); + return this.emitCallLike( + inst, + rt.object_setprop, + [this.val(inst.operands[0]), key, this.val(inst.operands[1])], + "setprop" + ); + } + + case "delete_prop": { + return this.emitCallLike( + inst, + rt.unopdelete, + [this.val(inst.operands[0]), this.val(inst.operands[1])], + "delres" + ); + } + + case "module_get_exotic": { + // the module object itself as an ejsval (namespace imports). + // JS modules have a link-time global (matches the opencoded + // legacy handleModuleGetExotic); native modules only exist + // at runtime, resolved by name through module_get. + const moduleString = String(inst.imms["module"]); + let module_global: import("@llvm").GlobalVariable | undefined; + if (moduleString === "%self") module_global = this.v.this_module_global; + else module_global = this.v.import_module_globals.get(moduleString); + if (module_global) { + let rv = this.v.emitEjsvalFromPtr(module_global, "exotic"); + this.values.set(inst, rv); + return rv; + } + let name = this.v.getAtom(String(moduleString)); + return this.emitCallLike(inst, rt.module_get, [name], "exotic"); + } + + case "module_slot_load": { + const slot_ref = this.moduleSlotRef(String(inst.imms["module"]), inst.imms["slot"] as number); + this.values.set(inst, ir.createLoad(types.EjsValue, slot_ref, "module_slot")); + return; + } + case "module_slot_store": { + const slot_ref = this.moduleSlotRef(String(inst.imms["module"]), inst.imms["slot"] as number); + ir.createStore(this.val(inst.operands[0]), slot_ref); + this.values.set(inst, this.val(inst.operands[0])); + return; + } + + case "get_global": { + let key = this.v.getAtom(String(inst.imms["atom"])); + return this.emitCallLike(inst, rt.global_getprop, [key], "getglobal"); + } + case "set_global": { + let key = this.v.getAtom(String(inst.imms["atom"])); + return this.emitCallLike( + inst, + rt.global_setprop, + [key, this.val(inst.operands[0])], + "setglobal" + ); + } + + case "make_env": { + const n = inst.imms["size"] as number; + // envs are 39% of all allocations (the P0 + // census) — bump-allocate inline; the runtime call is + // the slow path/safepoint. -fno-inline-alloc is + // the compile-time bisect hook. + const slow = () => this.call(rt.make_closure_env, [consts.int32(n)], "env"); + const rv = passes().inlineAlloc ? this.v.emitEnvAllocInline(n, slow) : slow(); + this.values.set(inst, rv); + return; + } + case "env_load": { + // inline slot addressing, recomputed per use + // from the boxed env (a relocated env re-derives) — + // deletes a runtime call per access. -fno-inline-env-slots + // restores the runtime-call path. + let ref = passes().inlineEnvSlots + ? this.v.emitEnvSlotRef( + this.val(inst.operands[0]), + inst.imms["slot"] as number + ) + : this.call( + rt.get_env_slot_ref, + [this.val(inst.operands[0]), consts.int32((inst.imms["slot"] as number))], + "slotref" + ); + this.values.set(inst, ir.createLoad(types.EjsValue, ref, "slot")); + return; + } + case "env_store": { + let ref = passes().inlineEnvSlots + ? this.v.emitEnvSlotRef( + this.val(inst.operands[0]), + inst.imms["slot"] as number + ) + : this.call( + rt.get_env_slot_ref, + [this.val(inst.operands[0]), consts.int32((inst.imms["slot"] as number))], + "slotref" + ); + ir.createStore(this.val(inst.operands[1]), ref); + this.emitStoreBarrier(this.val(inst.operands[0]), this.val(inst.operands[1])); + this.values.set(inst, this.val(inst.operands[1])); + return; + } + case "make_closure": { + let target = this.llvm_fns.get((inst.imms["fn"] as string)); + if (!target) throw new Error(`EIR emit: unknown closure target ${(inst.imms["fn"] as string)}`); + let name = this.v.getAtom( + String(inst.imms.name !== undefined ? inst.imms.name : (inst.imms["fn"] as string)) + ); + let rv = this.call( + rt.make_closure, + [this.val(inst.operands[0]), name, target], + "closure" + ); + this.values.set(inst, rv); + return; + } + + case "call": { + const direct = inst.imms["direct"] as string | undefined; + if (direct) { + const target = this.llvm_fns.get(direct); + if (!target) + throw new Error(`EIR emit: unknown direct callee ${direct}`); + let env_val = this.val(inst.operands[0]); + let this_val = this.val(inst.operands[1]); + let dargs = inst.operands.slice(2).map((o) => this.val(o)); + ir.createStore(this_val, this.this_slot); + let dargv; + if (dargs.length > 0) dargv = this.spillArgs(dargs); + else dargv = ir.createPointerCast(this.this_slot, types.EjsValue.pointerTo(), "noargs"); + return this.emitCallLike( + inst, + target, + [env_val, this.this_slot, consts.int32(dargs.length), dargv, this.undef()], + "dcall" + ); + } + let callee = this.val(inst.operands[0]); + let this_val = this.val(inst.operands[1]); + let args = inst.operands.slice(2).map((o) => this.val(o)); + ir.createStore(this_val, this.this_slot); + let argv; + if (args.length > 0) argv = this.spillArgs(args); + else argv = ir.createPointerCast(this.this_slot, types.EjsValue.pointerTo(), "noargs"); + return this.emitCallLike( + inst, + rt.invoke_closure, + [callee, this.this_slot, consts.int32(args.length), argv, this.undef()], + "callres" + ); + } + case "call_typed": { + // direct call to a specialized clone — args are + // raw machine values in registers, no scratch spill, no + // closure dispatch. Operand 0 (the EIR-level env slot) is + // NOT passed: clone signatures carry the formals alone + // (env is required-unused by the specialization checks) + const target = this.llvm_fns.get(inst.imms["fn"] as string); + if (!target) + throw new Error(`EIR emit: unknown call_typed callee ${String(inst.imms["fn"])}`); + const argv = inst.operands.slice(1).map((o) => this.val(o)); + return this.emitCallLike(inst, target, argv, "tcall"); + } + case "construct": { + let callee = this.val(inst.operands[0]); + let args = inst.operands.slice(1).map((o) => this.val(o)); + ir.createStore(this.undef(), this.this_slot); + let argv; + if (args.length > 0) argv = this.spillArgs(args); + else argv = ir.createPointerCast(this.this_slot, types.EjsValue.pointerTo(), "noargs"); + return this.emitCallLike( + inst, + rt.construct_closure, + [callee, this.this_slot, consts.int32(args.length), argv, callee], + "ctorres" + ); + } + case "construct_super": { + // the super constructor writes the constructed object back + // through OUR incoming &this (that's how a derived ctor's + // result reaches the runtime's construct machinery), and + // newTarget passes through unchanged + let callee = this.val(inst.operands[0]); + let args = inst.operands.slice(1).map((o) => this.val(o)); + let argv; + if (args.length > 0) argv = this.spillArgs(args); + else argv = ir.createPointerCast(this.this_slot, types.EjsValue.pointerTo(), "noargs"); + return this.emitCallLike( + inst, + rt.construct_closure, + [callee, this.fn_this_ptr, consts.int32(args.length), argv, this.fn_new_target], + "csuper" + ); + } + case "construct_super_apply": { + // operands = [super_ctor, args_array]; the runtime asserts + // argc == 1 and spreads the dense array itself + let callee = this.val(inst.operands[0]); + let argv = this.spillArgs([this.val(inst.operands[1])]); + return this.emitCallLike( + inst, + rt.construct_closure_apply, + [callee, this.fn_this_ptr, consts.int32(1), argv, this.fn_new_target], + "csuperapply" + ); + } + case "construct_apply": { + // like construct, but the args arrive as one dense array + // the runtime spreads; this_slot supplies the out-param + let callee = this.val(inst.operands[0]); + let argv = this.spillArgs([this.val(inst.operands[1])]); + ir.createStore(this.undef(), this.this_slot); + return this.emitCallLike( + inst, + rt.construct_closure_apply, + [callee, this.this_slot, consts.int32(1), argv, callee], + "ctorapply" + ); + } + case "new_target": { + this.values.set(inst, this.fn_new_target); + return; + } + + case "make_array": { + let elems = inst.operands.map((o) => this.val(o)); + if ((inst.imms["indices"] as readonly number[]) === undefined) { + let argv; + if (elems.length > 0) argv = this.spillArgs(elems); + else argv = ir.createPointerCast(this.this_slot, types.EjsValue.pointerTo(), "noargs"); + return this.emitCallLike( + inst, + rt.array_new_copy, + [consts.int64(elems.length), argv], + "arr" + ); + } + // an array literal with holes: force-filled allocation plus + // per-index stores for the non-holes (matching the legacy + // visitArrayExpression) + let arr = this.call( + rt.array_new, + [consts.int64((inst.imms["len"] as number)), consts.bool(true)], + "arr" + ); + this.values.set(inst, arr); + for (let i = 0; i < elems.length; i++) { + const key = this.v.loadDoubleEjsValue((inst.imms["indices"] as readonly number[])[i]!); + this.call(rt.object_setprop, [arr, key, elems[i]!], ""); + } + return arr; + } + case "define_accessor_computed": { + // a partial accessor descriptor: only the getter or only + // the setter is present; the runtime merges into any + // existing accessor property. enumerable+configurable, + // like the atom-keyed case. + let isGet = (inst.imms["kind"] as string) === "get"; + let flags = 0x33 | (isGet ? 0x100 : 0x200); + let accessor = this.val(inst.operands[2]); + let undef = this.v.loadUndefinedEjsValue(); + return this.emitCallLike( + inst, + rt.object_define_accessor_prop_desc, + [ + this.val(inst.operands[0]), + this.val(inst.operands[1]), + isGet ? accessor : undef, + isGet ? undef : accessor, + consts.int32(flags), + ], + "define_accessor_computed" + ); + } + case "define_accessor": { + // flags 0x19 = enumerable | configurable, matching the + // legacy visitObjectExpression + let key = this.v.getAtom(String(inst.imms["atom"])); + return this.emitCallLike( + inst, + rt.object_define_accessor_prop, + [ + this.val(inst.operands[0]), + key, + this.val(inst.operands[1]), + this.val(inst.operands[2]), + consts.int32(0x19), + ], + "defaccessor" + ); + } + case "array_from_spread": { + // concatenate the operands (array chunks / iterables) into + // a fresh array, like the legacy handleArrayFromSpread + let elems = inst.operands.map((o) => this.val(o)); + let argv; + if (elems.length > 0) argv = this.spillArgs(elems); + else argv = ir.createPointerCast(this.this_slot, types.EjsValue.pointerTo(), "noargs"); + return this.emitCallLike( + inst, + rt.array_from_iterables, + [consts.int32(elems.length), argv], + "spreadarr" + ); + } + case "make_object": { + let proto = ir.createLoad( + types.EjsValue, + this.v.ejs_globals["Object_prototype"]!, + "objproto" + ); + let obj = this.call(rt.object_create, [proto], "obj"); + this.values.set(inst, obj); + for (let i = 0; i < inst.operands.length; i++) { + let key = this.v.getAtom(String((inst.imms["keys"] as readonly string[])[i])); + this.call(rt.object_setprop, [obj, key, this.val(inst.operands[i])], ""); + } + return; + } + + // --- control flow --------------------------------------------------- + + case "br": { + const t = inst.targets![0]!; + this.addEdgeIncomings(inst, t); + ir.createBr(this.blocks.get(t.block)!); + return; + } + case "cond_br": { + let cond = this.val(inst.operands[0]); + // prop_iter_next produces the runtime's i8 EJSBool; every + // other condition source (to_boolean) is already an i1 + if (inst.operands[0]!.op === "prop_iter_next") + cond = ir.createICmpEq(cond, consts.True(), "moreleft_i1"); + this.addEdgeIncomings(inst, inst.targets![0]); + this.addEdgeIncomings(inst, inst.targets![1]); + ir.createCondBr( + cond, + this.blocks.get(inst.targets![0]!.block)!, + this.blocks.get(inst.targets![1]!.block)! + ); + return; + } + case "return": { + // the return value is read BEFORE the unlink (it + // may itself load from a frame slot); then pop the frame + const rv = this.val(inst.operands[0]); + if (this.gc_frame) this.v.emitGCFrameUnlink(this.gc_frame); + // an f64-result clone returns the raw double directly (a + // plain scalar return needs none of the ABI's ejsval + // struct-return handling) + if (this.eirFn.sig && this.eirFn.sig.result === "f64") ir.createRet(rv); + else this.abi.createRet(this.llvmFn, rv); + return; + } + case "throw": { + let throw_fn = this.v.ejs_runtime.throw; + if (inst.targets && inst.targets.length > 0) { + // unwinds to a local handler + let unwind = inst.targets[0]; + this.addEdgeIncomings(inst, unwind); + let cont = new llvm.BasicBlock("throw_unreachable", this.llvmFn); + this.abi.createInvoke( + this.llvmFn, + throw_fn.type, + throw_fn, + [this.val(inst.operands[0])], + cont, + this.blocks.get(unwind!.block)!, + "" + ); + ir.setInsertPoint(cont); + ir.createUnreachable(); + } else { + this.call(throw_fn, [this.val(inst.operands[0])], ""); + ir.createUnreachable(); + } + return; + } + case "unreachable": { + ir.createUnreachable(); + return; + } + + case "template_callsite": { + // mirror the legacy handleTemplateCallsite: a zeroinit + // per-site global, built lazily (a zeroed ejsval reads as + // number 0.0 — the is-number check doubles as + // "uninitialized"), arrays frozen, cooked.raw = raw + let cooked_strs = (inst.imms["cooked"] as readonly string[]); + let raw_strs = (inst.imms["raw"] as readonly string[]); + let g = new llvm.GlobalVariable( + this.module, + types.EjsValue, + `_ejs_eir_callsite_${mangle_gen++}`, + llvm.Constant.getAggregateZero(types.EjsValue), + false + ); + let loaded = this.v.createEjsValueLoad(g, "callsite_load"); + let then_bb = new llvm.BasicBlock("callsite_build", this.llvmFn); + let merge_bb = new llvm.BasicBlock("callsite_merge", this.llvmFn); + const from_bb = ir.getInsertBlock()!; + let isnum = this.v.isNumber(loaded); + ir.createCondBr(isnum, then_bb, merge_bb); + + ir.setInsertPoint(then_bb); + this.call(rt.gc_add_root, [g], ""); + const mkarr = (strs: readonly string[], name: string) => { + const vals = strs.map((s) => this.v.getAtom(String(s))); + let argv: import("@llvm").Value; + if (vals.length > 0) argv = this.spillArgs(vals); + else argv = ir.createPointerCast(this.this_slot, types.EjsValue.pointerTo(), "noargs"); + return this.call(rt.array_new_copy, [consts.int64(vals.length), argv], name); + }; + let cooked = mkarr(cooked_strs, "callsite_cooked"); + let raw = mkarr(raw_strs, "callsite_raw"); + let frozen_raw = this.call(rt.object_freeze, [raw], "frozen_raw"); + this.call(rt.object_setprop, [cooked, this.v.getAtom("raw"), frozen_raw], ""); + let frozen = this.call(rt.object_freeze, [cooked], "frozen_cooked"); + ir.createStore(frozen, g); + const built_bb = ir.getInsertBlock()!; + ir.createBr(merge_bb); + + ir.setInsertPoint(merge_bb); + let phi = ir.createPhi(types.EjsValue, 2, "callsite"); + phi.addIncoming(loaded, from_bb); + phi.addIncoming(frozen, built_bb); + this.values.set(inst, phi); + return; + } + case "make_regexp": { + let source = consts.string(ir, (inst.imms["source"] as string)); + let flags = consts.string(ir, (inst.imms["flags"] as string)); + return this.emitCallLike(inst, rt.regexp_new_utf8, [source, flags], "regexp"); + } + + case "rest_args": { + // rest = argc > index ? array_new_copy(argc - index, args + index) + // : array_new_copy(0, args) + // (count of zero never dereferences the pointer, so the + // select keeps this branch-free) + let index = (inst.imms["index"] as number); + let has_rest = ir.createICmpSGt(this.fn_argc, consts.int32(index), "has_rest"); + let count = ir.createNswSub(this.fn_argc, consts.int32(index), "rest_count"); + count = ir.createSelect(has_rest, count, consts.int32(0), "rest_count_sel"); + count = ir.createZExt(count, types.Int64, "rest_count64"); + let gep = ir.createGetElementPointer( + types.EjsValue, + this.fn_args_ptr, + [consts.int64(index)], + "rest_args" + ); + let ptr = ir.createSelect(has_rest, gep, this.fn_args_ptr, "rest_ptr"); + let rv = this.call(rt.array_new_copy, [count, ptr], "rest"); + this.values.set(inst, rv); + return rv; + } + + case "args_obj": { + return this.emitCallLike( + inst, + rt.arguments_new, + [this.fn_argc, this.fn_args_ptr], + "argsobj" + ); + } + + case "arg_len": { + // max(argc - index, 0) boxed, computed by a pure runtime + // helper (the argc register is the only input — no argv + // read, no allocation) + const index = (inst.imms["index"] as number) || 0; + const rv = this.call(rt.arg_length, [this.fn_argc, consts.int32(index)], "arg_len"); + this.values.set(inst, rv); + return rv; + } + + case "prop_iter_new": { + return this.emitCallLike( + inst, + rt.prop_iterator_new, + [this.val(inst.operands[0])], + "propiter" + ); + } + case "prop_iter_next": { + // returns the runtime's i1 directly; consumed by cond_br + return this.emitCallLike( + inst, + rt.prop_iterator_next, + [this.val(inst.operands[0]), consts.True()], + "moreleft" + ); + } + case "prop_iter_current": { + return this.emitCallLike( + inst, + rt.prop_iterator_current, + [this.val(inst.operands[0])], + "propcur" + ); + } + + case "call_runtime": { + // a direct call to a named entry in the runtime method table + const rtName = String(inst.imms["name"]); + const callee = (rt as unknown as Record)[rtName]; + if (!callee) throw new Error(`EIR emit: no runtime function '${rtName}'`); + let argv = inst.operands.map((o) => this.val(o)); + if ((inst.imms["void"] as boolean | undefined)) { + // void results can't be named (LLVM) or read as values. + // materialize the placeholder BEFORE the call: an + // invoke (in a protected region) terminates the block. + let undef_val = this.undef(); + this.emitCallLike(inst, callee, argv, ""); + this.values.set(inst, undef_val); + return; + } + return this.emitCallLike(inst, callee, argv, "rtres"); + } + + default: { + // generic binops / unops through the runtime interfaces + let binop = binop_for_op[inst.op]; + if (binop) { + let callee = this.v.ejs_binops[binop]; + if (!callee) throw new Error(`EIR emit: no binop interface for ${binop}`); + return this.emitCallLike( + inst, + callee, + [this.val(inst.operands[0]), this.val(inst.operands[1])], + "binres" + ); + } + const unop = unop_for_op[inst.op]; + if (unop) { + const callee = (this.v.ejs_runtime as unknown as Record)[`unop${unop}`]; + if (!callee) throw new Error(`EIR emit: no unop interface for ${unop}`); + return this.emitCallLike(inst, callee, [this.val(inst.operands[0])], "unres"); + } + throw new Error(`EIR emit: unhandled opcode '${inst.op}'`); + } + } + } +} diff --git a/lib/eir/errors.ts b/lib/eir/errors.ts new file mode 100644 index 00000000..1ab8a832 --- /dev/null +++ b/lib/eir/errors.ts @@ -0,0 +1,36 @@ +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: + */ + +// thrown by scope analysis / lowering when a construct is outside the +// supported subset; compile() reports it as a compile error. +// +// deliberately NOT an Error subclass: this shape predates the legacy +// pipeline's removal (subclassing Error miscompiled there) and is now +// simply the stable, structurally-testable form of the signal. + +import type { SourceLocation } from "../estree"; + +export interface LowerNotSupportedError extends Error { + eir_lower_not_supported: true; + what: string; +} + +export function LowerNotSupported( + what: string, + loc?: SourceLocation | null +): LowerNotSupportedError { + const locstr = loc && loc.start ? ` at ${loc.start.line}:${loc.start.column}` : ""; + const e = new Error(`EIR lowering does not support ${what}${locstr}`) as LowerNotSupportedError; + e.eir_lower_not_supported = true; + e.what = what; + return e; +} + +export function isLowerNotSupported(e: unknown): e is LowerNotSupportedError { + return ( + typeof e === "object" && + e !== null && + (e as { eir_lower_not_supported?: boolean }).eir_lower_not_supported === true + ); +} diff --git a/lib/eir/integrate.ts b/lib/eir/integrate.ts new file mode 100644 index 00000000..dd807291 --- /dev/null +++ b/lib/eir/integrate.ts @@ -0,0 +1,653 @@ +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: + */ + +// EIR integration: lower the whole module — toplevel statements, +// import/export init, every nested function, and the export accessor +// functions — as one EIR unit (see collectEIRToplevel). +// +// module-scope free names resolve through the refs map built here: +// - true globals (console, Math, ...): lowered as get_global; +// - named imports from non-native modules: lowered as module_slot_load +// (or folded, when the export is a const literal); +// - this module's own exported bindings: module_slot_load/store against +// the "%self" module global; +// - non-exported module-level bindings with literal initializers that +// are never reassigned: folded to the literal; +// - non-exported module-level vars promoted to hidden slots by +// gather-imports: module_slot_load/store on "%self". +// anything else unsupported throws LowerNotSupported, which compile() +// reports as a compile error — there is no other pipeline. + +import * as b from "../ast-builder"; +import * as debug from "../debug"; +import { ScopeAnalysis } from "./scopes"; +import { lowerAnalyzedFunction } from "./lower"; +import { specializeModule } from "./specialize"; +import type { SpecStats } from "./specialize"; +import type { TypeOracle } from "./oracle"; +import type { ModuleRef, ModCtx } from "./lower"; +import { isLowerNotSupported } from "./errors"; +import { Module } from "./ir"; +import { FunctionBuilder } from "./builder"; +import { verifyModule } from "./verifier"; +import { injectLowTierProbes } from "./lowtier-probe"; +import { eliminateDeadInFunction, optimizeModule } from "./optimize"; +import { devirtualizeModule } from "./devirt"; +import { sinkConstructResults } from "./sink-construct"; +import { printModule } from "./printer"; +import type * as e from "../estree"; +import type { ModuleInfo } from "../module-info"; +import type { CompilerOptions } from "../options"; +import { passes } from "../pass-config"; + +// one export's accessor pair, by EIR function name (compiler.ts resolves +// them against the emitted module in emitModuleResolution) +export interface ModuleAccessor { + key: string; + getter: string; + setter: string; +} + +export type CollectResult = + | { + eir_module: Module; + accessors: ModuleAccessor[]; + diamonds: number; + // shape-guard telemetry (all zero/empty when --types is off) + shape_sites: number; + shape_guards: number; + // 2-way polymorphic chains (subset of guards) + shape_poly_guards: number; + shape_declined: Record; + // born-with-shape telemetry + born_shaped: number; + ctor_fills: number; + fence_declined: Record; + // typed (raw f64) slot accesses emitted + typed_loads: number; + typed_stores: number; + // construct sites virtualized by constructor-result sinking + ctor_sunk: number; + // specialization stats (null when --types is off or nothing qualified) + spec: SpecStats | null; + error?: undefined; + } + | { + error: string; + eir_module?: undefined; + accessors?: undefined; + diamonds?: undefined; + shape_sites?: undefined; + shape_guards?: undefined; + shape_poly_guards?: undefined; + shape_declined?: undefined; + born_shaped?: undefined; + ctor_fills?: undefined; + fence_declined?: undefined; + typed_loads?: undefined; + typed_stores?: undefined; + ctor_sunk?: undefined; + spec?: undefined; + }; + +// --dump-after eir: print the lowered (verified) EIR module +function dumpRequested(options: CompilerOptions | undefined): boolean { + return !!(options && options.debug_passes && options.debug_passes.has("eir")); +} + +// --dump-after eir-opt: print the module again after optimization +function dumpOptRequested(options: CompilerOptions | undefined): boolean { + return !!(options && options.debug_passes && options.debug_passes.has("eir-opt")); +} + +function dumpModule(filename: string, mode: string, eir_module: Module): void { + console.log(`// EIR module for ${filename} (${mode})`); + console.log(printModule(eir_module)); +} + +// only primitive literals fold; regex literals are objects and need +// runtime construction +function isFoldableLiteral(n: e.Expression | null | undefined): n is e.Literal { + return !!n && n.type === "Literal" && (n.value === null || typeof n.value !== "object"); +} + +// the module-slot reference map: local name -> { module, slot, constval?, +// writable }. covers named imports and this module's own exported +// bindings. +function collectModuleRefs( + toplevelBody: e.Statement[], + module_infos: Map | null, + this_module_info: ModuleInfo | null +): Map { + let refs = new Map(); + + // imports. native modules ("@llvm" etc) share the ModuleInfo slot + // machinery with JS modules — named imports from either kind are slot + // loads, exactly like the legacy %moduleGetSlot path. namespace + // imports bind the module object itself (module_get_exotic); member + // accesses on it are ordinary property gets. + if (module_infos) { + for (let stmt of toplevelBody) { + if (stmt.type !== "ImportDeclaration") continue; + if (!stmt.source_path) continue; + let moduleString = stmt.source_path.value; + let module_info = module_infos.get(moduleString); + if (!module_info) continue; + for (let spec of stmt.specifiers) { + if (spec.type === "ImportNamespaceSpecifier") { + // module_info rides along so lowering can resolve + // ns.member accesses to slot loads at compile time + // (mirroring new-cc's visitMemberExpression rewrite — + // JS module objects don't support runtime property + // lookup of their exports) + refs.set(spec.local.name, { + exotic: moduleString, + module_info: module_info, + writable: false, + }); + continue; + } + // named/default imports resolve through slot loads, which + // need the module's link-time global — natives don't have + // one (their module object only exists at runtime) + if (module_info.isNative()) continue; + let imported_name; + if (spec.type === "ImportSpecifier") imported_name = spec.imported.name; + else if (spec.type === "ImportDefaultSpecifier") imported_name = "default"; + else continue; + let export_info = module_info.exports.get(imported_name); + if (!export_info || export_info.promoted) continue; + const entry: import("./lower").SlotRef = { + module: moduleString, + slot: export_info.slot_num, + writable: false, + }; + // const exports fold to their literal at compile time + // (matches new-cc's constval propagation) + if (isFoldableLiteral(export_info.constval)) + entry.constval = export_info.constval; + refs.set(spec.local.name, entry); + } + } + } + + // this module's own exported let/var/const bindings, via the "%self" + // module global. only declaration-form exports resolve this way; + // specifier-only exports (`export { X }`) alias another binding whose + // own resolution stands. exported names shadow same-named imports, + // so these are set second. + if (this_module_info) { + for (let wrapped of toplevelBody) { + if (wrapped.type !== "ExportNamedDeclaration") continue; + let decl = wrapped.declaration; + if (!decl || Array.isArray(decl)) continue; + if (decl.type === "VariableDeclaration") { + let is_const = decl.kind === "const"; + for (let d of decl.declarations) { + if (d.id.type !== "Identifier") continue; + if (!this_module_info.exports.has(d.id.name)) continue; + const export_info = this_module_info.exports.get(d.id.name)!; + const entry: import("./lower").SlotRef = { + module: "%self", + slot: export_info.slot_num, + writable: !is_const, + }; + if (is_const && isFoldableLiteral(export_info.constval)) { + entry.constval = export_info.constval; + entry.writable = false; + } + refs.set(d.id.name, entry); + } + } else if ( + (decl.type === "FunctionDeclaration" || decl.type === "ClassDeclaration") && + decl.id + ) { + // an exported function/class read in value position loads + // the slot the legacy toplevel stored the (single) closure + // in — identity-correct, unlike minting a new closure per + // reference. writes fall back (writable: false). + if (!this_module_info.exports.has(decl.id.name)) continue; + const export_info = this_module_info.exports.get(decl.id.name)!; + refs.set(decl.id.name, { + module: "%self", + slot: export_info.slot_num, + writable: false, + }); + } + } + } + + // non-exported module-level vars promoted to hidden slots by + // gather-imports: read/write through the "%self" module global, the + // same storage the legacy pipeline uses after the DesugarImportExport + // rewrite. const-declared ones (non-literal initializers) are + // read-only. + if (this_module_info) { + for (let stmt of toplevelBody) { + if (stmt.type === "VariableDeclaration") { + for (let d of stmt.declarations) { + if (d.id.type !== "Identifier") continue; + if (refs.has(d.id.name)) continue; + let export_info = this_module_info.exports.get(d.id.name); + if (!export_info || !export_info.promoted) continue; + refs.set(d.id.name, { + module: "%self", + slot: export_info.slot_num, + writable: stmt.kind !== "const", + }); + } + } else if ( + (stmt.type === "FunctionDeclaration" || stmt.type === "ClassDeclaration") && + stmt.id + ) { + if (refs.has(stmt.id.name)) continue; + let export_info = this_module_info.exports.get(stmt.id.name); + if (!export_info || !export_info.promoted) continue; + refs.set(stmt.id.name, { + module: "%self", + slot: export_info.slot_num, + writable: true, + }); + } + } + } + + return refs; +} + +// non-exported module-level bindings with literal initializers that are +// never reassigned: fold-only refs (no slot) +function addModuleConstLiterals( + toplevelBody: e.Statement[], + assigned: Set, + refs: Map +): void { + for (let stmt of toplevelBody) { + if (stmt.type !== "VariableDeclaration") continue; // exported ones already in refs + for (let d of stmt.declarations) { + if (d.id.type !== "Identifier") continue; + if (!isFoldableLiteral(d.init)) continue; + if (assigned.has(d.id.name)) continue; + if (refs.has(d.id.name)) continue; + refs.set(d.id.name, { module: null, slot: -1, constval: d.init, writable: false }); + } + } +} + +// module-scope names that are ever assigned at the top level; calls into +// those can't be made direct and their literals can't fold +function collectAssignedNames(toplevelBody: e.Statement[]): Set { + const assigned = new Set(); + // reflective object-graph walk (the same legitimate-unknown seam as + // gather-imports' var scanner) + const walk = (n: unknown): void => { + if (!n || typeof n !== "object") return; + if (Array.isArray(n)) { + for (const el of n) walk(el); + return; + } + const node = n as e.Node; + // conservatively descend everywhere, including into nested + // functions: a nested assignment to a module-scope name still + // invalidates direct calls / const folding. + if (node.type === "AssignmentExpression" && node.left && node.left.type === "Identifier") + assigned.add(node.left.name); + if (node.type === "UpdateExpression" && node.argument && node.argument.type === "Identifier") + assigned.add(node.argument.name); + for (const k of Object.keys(node)) { + if (k === "loc") continue; + walk((node as unknown as Record)[k]); + } + }; + walk(toplevelBody); + return assigned; +} + +// each non-promoted export gets a getter (and setter) function on the +// module object so importers resolve it lazily. these used to be tiny +// AST FunctionExpressions compiled by the legacy visitor — the last +// thing it compiled; they're built directly as EIR now. getters fold +// primitive const exports (matching the legacy getExportGetter); +// everything else loads the export's slot on "%self". +function uniqueFnName(eir_module: Module, base: string): string { + let names = new Set(eir_module.functions.map((f) => f.name)); + let name = base; + for (let i = 1; names.has(name); i++) name = `${base}$${i}`; + return name; +} + +function buildModuleAccessors(eir_module: Module, this_module_info: ModuleInfo): ModuleAccessor[] { + const accessors: ModuleAccessor[] = []; + this_module_info.exports.forEach((export_info, key) => { + if (export_info.promoted) return; // hidden slot: no accessors + + let getter_name = uniqueFnName(eir_module, `get_export_${key}`); + { + let fb = new FunctionBuilder(getter_name, ["%env", "%this"]); + let cv = export_info.constval; + let v; + if (cv && cv.type === "Literal" && cv.value === null) v = fb.constNull(); + else if (cv && cv.type === "Literal" && typeof cv.value === "number") + v = fb.constNumber(cv.value); + else if (cv && cv.type === "Literal" && typeof cv.value === "string") + v = fb.constAtom(cv.value); + else if (cv && cv.type === "Literal" && typeof cv.value === "boolean") + v = fb.constBool(cv.value); + else + v = fb.emit("module_slot_load", [], { + module: "%self", + slot: export_info.slot_num, + }); + fb.emit("return", [v], {}); + eir_module.addFunction(fb.fn); + } + + let setter_name = uniqueFnName(eir_module, `set_export_${key}`); + { + let fb = new FunctionBuilder(setter_name, ["%env", "%this", "value"]); + let v = fb.readVariable("value", fb.cur); + fb.emit("module_slot_store", [v], { + module: "%self", + slot: export_info.slot_num, + }); + fb.emit("return", [fb.constUndefined()], {}); + eir_module.addFunction(fb.fn); + } + + accessors.push({ key: key, getter: getter_name, setter: setter_name }); + }); + return accessors; +} + +// lower the WHOLE module — toplevel statements, import/export init, and +// every nested function — as one EIR unit. module-scope bindings +// resolve through the refs machinery (slots for exported/promoted +// names, const-literal folds); everything else is an ordinary toplevel +// local, captured into the toplevel's environment as needed. returns +// { eir_module, accessors } on success or { error } when something +// doesn't lower — which compile() turns into a compile error. the +// emitted module is wrapped by compiler.js's module-resolution +// scaffolding (see emitEIRToplevel). +// `export default function f() {}` declares a module-scope binding AND +// stores the default-export slot. normalize to the two statements that +// say exactly that; unnamed `export default function () {}` is just an +// expression-form default export. +function normalizeDefaultExports(body: e.Statement[]): void { + for (let i = 0; i < body.length; i++) { + const stmt = body[i]!; + if (stmt.type !== "ExportDefaultDeclaration") continue; + let decl = stmt.declaration; + if (!decl) continue; + if (decl.type === "FunctionDeclaration") { + if (decl.id) { + stmt.declaration = b.identifier(decl.id.name); + body.splice(i, 0, decl); + i++; + } else { + // an unnamed default function is just an expression-form + // default export (in-place retype) + (decl as { type: string }).type = "FunctionExpression"; + } + } else if ( + decl.type === "VariableDeclaration" && + decl.declarations.length === 1 && + decl.declarations[0]!.id.type === "Identifier" + ) { + // `export default class Foo {}` arrives here post-DesugarClasses + // as `let Foo = ` + stmt.declaration = b.identifier((decl.declarations[0]!.id as e.Identifier).name); + body.splice(i, 0, decl); + i++; + } + } +} + +export function collectEIRToplevel( + tree: e.Program, + filename: string, + module_infos: Map | null, + this_module_info: ModuleInfo, + options: CompilerOptions, + // the module's type oracle (null = no typed fast paths). + // NB: normalizeDefaultExports below splices/retypes a few toplevel + // statements AFTER the probe analyzed the tree — surviving nodes keep + // their identity; nodes minted here read as oracle-unknown (-> top, + // no diamond), visible in the probe's oracleUnknown counter. + oracle: TypeOracle | null = null +): CollectResult { + const toplevel = tree.body[0] as e.FunctionDeclaration; + const body = toplevel.body.body; + normalizeDefaultExports(body); + + let assigned = collectAssignedNames(body); + let refs = collectModuleRefs(body, module_infos, this_module_info); + addModuleConstLiterals(body, assigned, refs); + + let moduleSlotNames = new Set(refs.keys()); + + try { + let analysis = new ScopeAnalysis(); + let info = analysis.analyzeToplevel(toplevel, toplevel.id.name, moduleSlotNames); + + // module functions call each other through their slots + // (slot-load + invoke_closure): a slot-backed function may capture + // the toplevel environment, which a direct caller's envParam + // wouldn't carry. direct calls stay a devirtualization + // opportunity for the optimizer, which can prove capture shapes. + let typed_stats: NonNullable = { + diamonds: 0, + trusted: 0, + }; + let mod_ctx = { + refs: refs, + this_module_info: this_module_info, + module_infos: module_infos, + oracle: oracle, + typed_stats: typed_stats, + // --types-dump grows the per-site shape census + shape_dump: !!options.types_dump, + }; + + let eir_module = new Module(filename); + lowerAnalyzedFunction(info, analysis, eir_module, mod_ctx); + let accessors = buildModuleAccessors(eir_module, this_module_info); + verifyModule(eir_module); + // the as-lowered dump must precede optimization (which mutates + // the module in place) + if (dumpRequested(options)) dumpModule(filename, "toplevel-as-EIR", eir_module); + + // testing: -flowtier swaps the bodies of the lowtier_* + // probe functions (test/eir-lowtier1.js) for hand-built low-tier + // EIR, so the low-tier ops can be executed end to end before + // lowering emits them. Same mold as -fno-eir-opt. + if (passes().lowtier) { + const n = injectLowTierProbes(eir_module); + if (n > 0) verifyModule(eir_module); + } + + // -fno-eir-opt disables the EIR optimizer without touching the + // LLVM pass pipeline (-O0 changes both; -fllvm-opt decouples the + // LLVM side) + let spec_stats: SpecStats | null = null; + if (passes().eirOpt) { + const stats = optimizeModule(eir_module, info.name); + if ( + stats.allocs_sunk || + stats.reads_folded || + stats.calls_inlined || + stats.iters_folded || + stats.dead_removed || + stats.guards_folded || + stats.regions_merged || + stats.raw_join_params || + stats.shape_guards_folded || + stats.shape_regions_merged || + stats.shape_numeric_merged || + stats.shape_allocs_sunk || + stats.shape_guards_sunk || + stats.args_sunk || + stats.flow_allocs_sunk || + stats.consts_folded || + stats.branches_folded || + stats.params_pruned || + stats.typeof_rewrites || + stats.lattice_arith || + stats.slot_loads_cse + ) + debug.log( + 1, + `EIR-opt: ${filename}: ${stats.calls_inlined} call(s) inlined, ` + + `${stats.allocs_sunk} alloc(s) sunk, ${stats.reads_folded} read(s) folded, ` + + `${stats.iters_folded} iterator walk(s) folded, ` + + `${stats.dead_removed} dead inst(s) removed, ` + + `${stats.guards_folded} guard(s) folded, ` + + `${stats.regions_merged} region(s) merged, ` + + `${stats.raw_join_params} raw f64 join param(s), ` + + `${stats.shape_guards_folded} shape guard(s) folded, ` + + `${stats.shape_regions_merged} shape region(s) merged, ` + + `${stats.shape_numeric_merged} shape+numeric region(s) merged, ` + + `${stats.shape_allocs_sunk} shaped alloc(s) sunk, ` + + `${stats.shape_guards_sunk} shape guard branch(es) resolved, ` + + `${stats.args_sunk} args object(s) sunk, ` + + `${stats.flow_allocs_sunk} flow-sunk alloc(s) ` + + `(${stats.allocs_materialized} materialized), ` + + `${stats.consts_folded} const(s) folded, ` + + `${stats.branches_folded} branch(es) folded, ` + + `${stats.params_pruned} trivial param(s) pruned, ` + + `${stats.typeof_rewrites} typeof test(s) rewritten, ` + + `${stats.lattice_arith} lattice-typed op(s) lowered, ` + + `${stats.slot_loads_cse} slot load(s) CSE'd` + ); + verifyModule(eir_module); + + // function specialization. Runs AFTER the first + // optimizer pass (EIR inlining has already taken the + // single-block calls it can — a make_closure with no remaining + // call uses is no longer a candidate) and only with an oracle + // (never on flag-off compiles). A second optimizer pass then + // cleans the clones (entry boxes prove numbers; residual + // guards fold; loop joins go raw; dead closures/loads drop). + // -fno-eir-spec bisects specialization alone. + if (oracle && passes().eirSpec) { + spec_stats = { specialized: 0, sites: 0, rejected: 0, wrapped: 0, fenced: 0 }; + const changed = specializeModule( + eir_module, + analysis, + oracle, + this_module_info, + mod_ctx, + spec_stats + ); + if (changed) { + verifyModule(eir_module); + optimizeModule(eir_module, info.name); + verifyModule(eir_module); + // untrusted (wrapper) clones need a second pass: the + // loop-carried number proofs that fold their entry + // guards only fit provenNumberAt's depth cap after + // cleanup has pruned the trivial join params, and + // cleanup runs at the tail of a pass. Wrapper-free + // compiles skip it (byte-pure). + if (spec_stats.wrapped > 0) { + optimizeModule(eir_module, info.name); + verifyModule(eir_module); + } + debug.log( + 1, + `EIR-spec: ${filename}: ${spec_stats.specialized} fn(s) specialized, ` + + `${spec_stats.sites} call site(s) rewritten, ` + + `${spec_stats.rejected} clone(s) rejected, ` + + `${spec_stats.wrapped} boundary wrapper(s), ` + + `${spec_stats.fenced} site(s) fenced` + ); + } + if ( + spec_stats.specialized === 0 && + spec_stats.rejected === 0 && + spec_stats.wrapped === 0 + ) + spec_stats = null; + } + + // constructor-result sinking: epoch-guarded + // virtualization of module-local shaped-constructor results + // (docs/sinking-plan.md). Runs after specialization — the + // hot construct sites live inside the clones — and re-runs + // the optimizer so the shaped-literal sink drains the + // planted virtual allocations. -fno-ctor-sink bisects + // (checked inside the pass). + if (oracle) { + const promoted = new Set(); + if (this_module_info) + this_module_info.exports.forEach((einfo) => { + if (einfo.promoted) promoted.add(einfo.slot_num); + }); + const n = sinkConstructResults(eir_module, promoted, info.name); + if (n > 0) { + verifyModule(eir_module); + optimizeModule(eir_module, info.name); + verifyModule(eir_module); + typed_stats.ctor_sunk = n; + debug.log( + 1, + `EIR-ctor-sink: ${filename}: ${n} construct site(s) virtualized` + ); + } + } + + // direct-call devirtualization (devirt.ts). Runs LAST — a + // devirtualized site no longer uses its closure/slot-load as + // a plain-call callee, which would make specialize.ts's + // closed-world enumeration decline the strictly-better + // call_typed rewrite. -fno-devirt bisects (checked inside + // the pass). + { + const dstats = devirtualizeModule(eir_module, info.name); + if (dstats.ssa_sites || dstats.slot_sites) { + // sweep the closures/loads the rewrites just orphaned + for (const fn of eir_module.functions) eliminateDeadInFunction(fn); + verifyModule(eir_module); + debug.log( + 1, + `EIR-devirt: ${filename}: ` + + `${dstats.ssa_sites + dstats.slot_sites} call site(s) devirtualized ` + + `(${dstats.ssa_sites} ssa, ${dstats.slot_sites} slot)` + ); + } + } + if (dumpOptRequested(options)) dumpModule(filename, "optimized", eir_module); + } + + toplevel.eir_module = eir_module; + toplevel.eir_main = info.name; + toplevel.body = { type: "BlockStatement", body: [], loc: toplevel.loc }; + debug.log( + 1, + `EIR: ${filename}: whole module lowered (toplevel-as-EIR)` + + (typed_stats.diamonds > 0 ? `, ${typed_stats.diamonds} typed diamond(s)` : "") + ); + return { + eir_module: eir_module, + accessors: accessors, + diamonds: typed_stats.diamonds, + shape_sites: typed_stats.shape_sites ?? 0, + shape_guards: typed_stats.shape_guards ?? 0, + shape_poly_guards: typed_stats.shape_poly_guards ?? 0, + shape_declined: typed_stats.shape_declined ?? {}, + born_shaped: typed_stats.born_shaped ?? 0, + ctor_fills: typed_stats.ctor_fills ?? 0, + fence_declined: typed_stats.fence_declined ?? {}, + typed_loads: typed_stats.typed_loads ?? 0, + typed_stores: typed_stats.typed_stores ?? 0, + ctor_sunk: typed_stats.ctor_sunk ?? 0, + spec: spec_stats, + }; + } catch (e) { + if (!isLowerNotSupported(e)) throw e; + // there is no legacy pipeline to fall back to anymore: surface + // the reason as a compile error at the call site + return { error: e.message }; + } +} + diff --git a/lib/eir/intrinsics.ts b/lib/eir/intrinsics.ts new file mode 100644 index 00000000..73ef3d76 --- /dev/null +++ b/lib/eir/intrinsics.ts @@ -0,0 +1,67 @@ +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: + */ + +// The %-intrinsic calls EIR knows how to lower, keyed by callee name. +// Pre-EIR desugar passes (see preEIRConvert in desugar.js) rewrite +// constructs lowering has no native form for into calls of these +// intrinsics. +// +// scopes.ts consults this table to reject unknown intrinsics EARLY (a +// late LowerNotSupported is a compile error with less context), so keep +// it the single source of truth: never lower an intrinsic in lower.ts +// that isn't listed here. + +import type { OpName } from "./ops"; + +interface OpIntrinsic { + // lower to this op; operands = the visited arguments + op: OpName; + runtime?: undefined; + // the op's result becomes the function's `this` (super() in a + // derived constructor initializes it) + rebindThis?: boolean; + void?: undefined; +} + +interface RuntimeIntrinsic { + op?: undefined; + // lower to call_runtime imms.name; the runtime function must take + // plain ejsval arguments and return an ejsval + runtime: string; + rebindThis?: undefined; + // the runtime function returns void (the call is only valid as a + // statement; its EIR value reads as undefined) + void?: boolean; +} + +export type IntrinsicEntry = OpIntrinsic | RuntimeIntrinsic; + +export const eir_intrinsics: Record = { + "%arrayFromSpread": { op: "array_from_spread" }, + + // DesugarClasses + "%objectCreate": { runtime: "object_create" }, + "%setPrototypeOf": { runtime: "object_set_prototype_of" }, + "%setConstructorKindBase": { runtime: "set_constructor_kind_base", void: true }, + "%setConstructorKindDerived": { runtime: "set_constructor_kind_derived", void: true }, + "%constructSuper": { op: "construct_super", rebindThis: true }, + "%constructSuperApply": { op: "construct_super_apply", rebindThis: true }, + + // DesugarSpread (new Foo(...args)) + "%constructApply": { op: "construct_apply" }, + + // DesugarMetaProperties (new.target) + "%getNewTarget": { op: "new_target" }, + + // DesugarGeneratorFunctions: coroutine-style — the generator body is + // an ordinary closure run on its own stack (runtime ucontext switch), + // so these are plain runtime calls + "%makeGenerator": { runtime: "make_generator" }, + "%generatorYield": { runtime: "generator_yield" }, + "%generatorIsReturnSentinel": { runtime: "generator_is_return_sentinel" }, + "%generatorReturnValue": { runtime: "generator_return_value" }, + + // DesugarDestructuring (array patterns iterate via a runtime wrapper) + "%createIteratorWrapper": { runtime: "iterator_wrapper_new" }, +}; diff --git a/lib/eir/ir.ts b/lib/eir/ir.ts new file mode 100644 index 00000000..3ee7be07 --- /dev/null +++ b/lib/eir/ir.ts @@ -0,0 +1,288 @@ +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: + */ + +// EIR core data structures: Module / Func / Block / Inst. +// SSA with basic block arguments (no phi nodes); block parameters are +// Insts with op "blockparam". See EIRProposal.md. + +import { opInfo, isTerminator } from "./ops"; + +// the immediate (non-value) attributes an instruction carries. values +// are op-specific: atoms and runtime-fn names are strings, env slots and +// array lengths are numbers, make_object keys / make_array indices are +// arrays, template_callsite carries string arrays, etc. +export type ImmValue = + | string + | number + | boolean + | null + | undefined + | readonly string[] + | readonly number[]; + +export type Imms = { [name: string]: ImmValue }; + +export type TargetKind = "normal" | "unwind" | undefined; + +export interface Target { + block: Block; + args: (Inst | null)[]; + kind: TargetKind; +} + +export interface PredEdge { + inst: Inst; + targetIndex: number; +} + +// one field of a module-interned guard shape, in +// insertion (transition-chain) order. repr mirrors the runtime's +// EJSShapeRepr and is part of shape identity. +export interface ShapeField { + name: string; + repr: "boxed" | "f64"; +} + +// the canonical Module.shapes key for a field list — also what has_shape/ +// slot_* carry in imms.shape, so printed IR is self-describing +export function shapeKeyOf(fields: readonly ShapeField[]): string { + return fields.map((f) => `${f.name}:${f.repr}`).join(","); +} + +export class Module { + name: string; + functions: Func[] = []; + // the guard shapes this module interns at init + // (imms.shape key -> ordered fields). The verifier checks slot + // bounds/reprs against this; the emitter mints one global + one + // _ejs_shape_intern call per entry (the atom-table precedent). + shapes = new Map(); + + constructor(name: string) { + this.name = name; + } + + addFunction(fn: Func): Func { + this.functions.push(fn); + return fn; + } + + // intern a field list into the module's shape table, returning the + // imms.shape key ops should carry + internShape(fields: readonly ShapeField[]): string { + const key = shapeKeyOf(fields); + if (!this.shapes.has(key)) this.shapes.set(key, fields.slice()); + return key; + } +} + +// a specialized clone's typed signature. `formals` types the +// JS formal parameters only (entry params [0]=%env and [1]=%this stay +// boxed/implicit; a clone's %this is required-unused by the static callee +// checks). This is the second controlled lift of the P2 +// raw-values-cannot-cross-blocks rule: an entry blockparam may be f64 +// exactly when the sig's matching formal says so, and the verifier +// re-checks every call_typed against the callee's sig. +export interface FuncSig { + formals: ("any" | "f64")[]; + result: "any" | "f64"; +} + +export class Func { + name: string; + paramNames: string[]; + blocks: Block[] = []; + next_value_id = 0; + next_block_id = 0; + entry: Block | null = null; + // non-null only on specialized clones (specialize.ts) + sig: FuncSig | null = null; + + constructor(name: string, paramNames?: string[]) { + this.name = name; + this.paramNames = paramNames || []; + } + + newValueId(): number { + return this.next_value_id++; + } + + addBlock(block: Block): Block { + this.blocks.push(block); + if (!this.entry) this.entry = block; + return block; + } + + // all instructions, params first per block, in block order. + forEachInst(cb: (inst: Inst, block: Block) => void): void { + for (const b of this.blocks) { + for (const p of b.params) cb(p, b); + for (const i of b.insts) cb(i, b); + } + } +} + +export class Block { + fn: Func; + // uniquified within the function so lowering can reuse friendly names + name: string; + params: Inst[] = []; + insts: Inst[] = []; + sealed = false; + // catch blocks are reached only by unwind edges; their first param + // is the caught exception, produced by the unwind machinery rather + // than passed as an edge argument. + isCatch = false; + // predecessor edges + predEdges: PredEdge[] = []; + // Braun SSA construction state (owned by the builder): + // varname -> param Inst + incompleteParams = new Map(); + + constructor(fn: Func, name?: string) { + this.fn = fn; + this.name = `${name || "bb"}${fn.next_block_id++}`; + } + + // edge args don't carry the exception param, so a param's position in + // an edge's args differs from its position in `params` on catch blocks. + argIndexOfParam(param: Inst): number { + return param.paramIndex - (this.isCatch ? 1 : 0); + } + + get terminator(): Inst | null { + const last = this.insts[this.insts.length - 1]; + if (last && isTerminator(last)) return last; + return null; + } + + get terminated(): boolean { + return this.terminator !== null; + } + + preds(): Block[] { + return this.predEdges.map((e) => e.inst.block!); + } + + succs(): Block[] { + const t = this.terminator; + if (!t || !t.targets) return []; + return t.targets.map((tgt) => tgt.block); + } + + addParam(nameHint?: string): Inst { + const p = new Inst(this.fn, "blockparam", [], {}); + p.block = this; + p.nameHint = nameHint; + p.paramIndex = this.params.length; + this.params.push(p); + // extend every known predecessor edge with a slot for this param. + // callers (the builder) fill the values in. + if (!p.isException) { + for (const e of this.predEdges) { + e.inst.targets![e.targetIndex]!.args.push(null); + } + } + return p; + } + + removeParam(param: Inst): void { + const idx = param.paramIndex; + const argIdx = this.argIndexOfParam(param); + this.params.splice(idx, 1); + for (let i = idx; i < this.params.length; i++) this.params[i]!.paramIndex = i; + for (const e of this.predEdges) { + e.inst.targets![e.targetIndex]!.args.splice(argIdx, 1); + } + param.removed = true; + } +} + +export class Inst { + id: number; + op: string; + // operand values. slots are filled by construction and never null in + // a verified function; the builder's edge machinery temporarily holds + // nulls in Target.args only. + operands: Inst[]; + imms: Imms; + block: Block | null = null; + type = "any"; // the (future) type lattice; untyped for now + // control-flow targets for terminators / invokes + targets: Target[] | null = null; + + // --- blockparam bookkeeping ------------------------------------------------ + nameHint: string | undefined = undefined; + paramIndex = -1; + // catch blocks' first param is the caught exception + isException = false; + removed = false; + // the raw-join pass: a block parameter that carries a RAW f64 across + // its incoming edges — the controlled lift of the + // raw-values-cannot-cross-blocks rule. Set only by the optimizer + // (optimize-guards.ts rawJoinParams); lowering must never set it, so + // every lowering-created edge keeps the strict boxed rule. The + // qualification is STRUCTURAL, not provenance-linked: any param + // whose every incoming argument provably carries an f64 (strippable + // box_f64 / f64 value / another converted param, rooted in a real + // f64 producer) may convert — guard-region merges create most such + // shapes, but e.g. a fully-proven loop-carried param qualifies too. + // The marker is not trusted on its own: the verifier independently + // checks the full safety conditions (type is f64, every incoming + // argument is f64, non-catch block, no unwind edges), so a stray + // marker can only ever *tighten* checking, never admit an ill-typed + // edge. The emitter types the phi as double. + rawJoin = false; + + constructor(fn: Func, op: string, operands?: Inst[], imms?: Imms) { + this.id = fn.newValueId(); + this.op = op; + this.operands = operands || []; + this.imms = imms || {}; + + const info = opInfo(op); + if (info.arity >= 0 && this.operands.length !== info.arity) + throw new Error( + `EIR: '${op}' expects ${info.arity} operands, got ${this.operands.length}` + ); + if (info.sig) this.type = info.sig.result; // the low tier's typed results + } + + addTarget(block: Block, args?: (Inst | null)[], kind?: TargetKind): void { + if (!this.targets) this.targets = []; + const targetIndex = this.targets.length; + this.targets.push({ block, args: args || [], kind }); + block.predEdges.push({ inst: this, targetIndex }); + } +} + +// replace every use of `from` (as an operand or edge argument) in fn with `to`. +export function replaceAllUses(fn: Func, from: Inst, to: Inst): void { + fn.forEachInst((inst) => { + for (let i = 0; i < inst.operands.length; i++) { + if (inst.operands[i] === from) inst.operands[i] = to; + } + if (inst.targets) { + for (const t of inst.targets) { + for (let i = 0; i < t.args.length; i++) { + if (t.args[i] === from) t.args[i] = to; + } + } + } + }); +} + +// collect the instructions that use `value` (operands or edge args). +export function usersOf(fn: Func, value: Inst): Inst[] { + const users: Inst[] = []; + fn.forEachInst((inst) => { + let uses = false; + for (const o of inst.operands) if (o === value) uses = true; + if (inst.targets) { + for (const t of inst.targets) for (const a of t.args) if (a === value) uses = true; + } + if (uses) users.push(inst); + }); + return users; +} diff --git a/lib/eir/liveness.ts b/lib/eir/liveness.ts new file mode 100644 index 00000000..c07d7f2d --- /dev/null +++ b/lib/eir/liveness.ts @@ -0,0 +1,120 @@ +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: + */ + +// which values must live in gc-frame slots? +// +// A value needs a precise, relocatable home iff it is LIVE ACROSS a +// safepoint — an op that lowers to a runtime call that may allocate +// (and therefore may run a minor collection that MOVES young objects). +// Values not live across any safepoint never coexist with a move; raw +// f64/i1 values are not references; consts rematerialize from statics. +// +// Soundness does not depend on this analysis being complete: any value +// left out keeps its SSA home, and a live-across-call SSA value is +// always visible to the conservative stack/register scan (ABI: it must +// be in a callee-saved register or a stack slot), which PINS its +// referent. Under-coverage costs pins, never correctness. That is +// also why v1 deliberately skips two classes: +// - ops with unwind targets (invoke form, try regions): their reload +// point is the normal edge, which may be shared — skipped values +// stay pinned; +// - values DEFINED by target-carrying ops: their def-site store has +// no natural insertion point in the defining block. + +import { Func, Inst } from "./ir"; +import { Effect, opInfo } from "./ops"; + +// a v1 safepoint: a target-less op whose lowering calls into the +// runtime with allocation possible. box_f64 carries GC in the effect +// table but emits pure bit arithmetic — never a safepoint. make_env +// counts even with the inline fast path: its slow path is the +// canonical safepoint, and covering both arms is correct (the fast +// path's reload folds back to the store). +export function isSafepoint(inst: Inst): boolean { + if (inst.targets && inst.targets.length > 0) return false; + if (inst.op === "box_f64") return false; + const info = opInfo(inst.op); + if (info.terminator) return false; + return (info.effects & (Effect.GC | Effect.CALL)) !== 0; +} + +function spillable(v: Inst): boolean { + if (v.type !== "any") return false; // raw f64/i1: not references + if (v.op === "const") return false; // rematerializes from statics + if (v.targets && v.targets.length > 0) return false; // invoke results stay pinned + return true; +} + +function usesOf(inst: Inst, fn: (v: Inst) => void): void { + for (const o of inst.operands) fn(o); + if (inst.targets) for (const t of inst.targets) for (const a of t.args) if (a) fn(a); +} + +// the set of values live across at least one safepoint, or null when +// the function needs no gc-frame +export function computeSpilledValues(fn: Func): Set | null { + let anySafepoint = false; + fn.forEachInst((inst) => { + if (isSafepoint(inst)) anySafepoint = true; + }); + if (!anySafepoint) return null; + + // backward liveness to fixpoint. sets keyed by inst; block liveOut + // maps kept in an array parallel to fn.blocks. + const liveIn = new Map>(); + const liveOut = new Map>(); + for (const b of fn.blocks) { + liveIn.set(b, new Set()); + liveOut.set(b, new Set()); + } + + let changed = true; + while (changed) { + changed = false; + // reverse block order is a decent schedule for backward flow + for (let bi = fn.blocks.length - 1; bi >= 0; bi--) { + const b = fn.blocks[bi]!; + const out = liveOut.get(b)!; + const before = out.size; + const term = b.terminator; + if (term && term.targets) { + for (const t of term.targets) { + const sIn = liveIn.get(t.block); + if (!sIn) continue; + for (const v of sIn) out.add(v); + // successor params are defs there, not live into us + for (const p of t.block.params) out.delete(p); + } + } + if (out.size !== before) changed = true; + + const live = new Set(out); + for (let i = b.insts.length - 1; i >= 0; i--) { + const inst = b.insts[i]!; + live.delete(inst); + usesOf(inst, (v) => live.add(v)); + } + const inSet = liveIn.get(b)!; + const inBefore = inSet.size; + for (const v of live) inSet.add(v); + if (inSet.size !== inBefore) changed = true; + } + } + + // record: for each safepoint, everything live just after it + const spilled = new Set(); + for (const b of fn.blocks) { + const live = new Set(liveOut.get(b)!); + for (let i = b.insts.length - 1; i >= 0; i--) { + const inst = b.insts[i]!; + // `live` here = live-after-inst + if (isSafepoint(inst)) { + for (const v of live) if (v !== inst && spillable(v)) spilled.add(v); + } + live.delete(inst); + usesOf(inst, (v) => live.add(v)); + } + } + return spilled.size > 0 ? spilled : null; +} diff --git a/lib/eir/lower.ts b/lib/eir/lower.ts new file mode 100644 index 00000000..bd951751 --- /dev/null +++ b/lib/eir/lower.ts @@ -0,0 +1,2412 @@ +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: + */ + +// AST -> EIR lowering. +// +// Covers a whitelisted subset of the (desugared) AST; anything else throws +// LowerNotSupported, which compile() reports as a compile error. +// +// Scope resolution (lib/eir/scopes.ts) runs first and decides, per binding: +// SSA local vs. environment slot. Lowering then emits make_env / +// env_load / env_store / make_closure directly. +// +// Calling convention mirrors the runtime: every function takes +// (%env, %this, ...params). + +import { FunctionBuilder } from "./builder"; +import { Module, Func, Block, Inst } from "./ir"; +import type { ShapeField } from "./ir"; +import { ScopeAnalysis, compound_assign_ops, Binding, FnInfo, LoopEnv } from "./scopes"; +import { LowerNotSupported } from "./errors"; +import { eir_intrinsics } from "./intrinsics"; +import type * as e from "../estree"; +import type { ModuleInfo } from "../module-info"; +import type { TypeOracle } from "./oracle"; +import { passes } from "../pass-config"; + +// --- module-scope interop types (integrate.ts imports these) ----------------- + +// a module-slot-backed (or const-folded) reference +export interface SlotRef { + module: string | null; // "%self", a module path, or null for fold-only + slot: number; + constval?: e.Literal; + writable: boolean; + exotic?: undefined; + module_info?: undefined; +} + +// a namespace import: the module object itself (module_get_exotic); +// member accesses resolve to slot loads at compile time +export interface ExoticRef { + exotic: string; + module_info: ModuleInfo; + writable: boolean; + module?: undefined; + slot?: undefined; + constval?: undefined; +} + +export type ModuleRef = SlotRef | ExoticRef; + +export interface ModCtx { + refs: Map; + this_module_info?: ModuleInfo | null; + module_infos?: Map | null; + // the per-module type oracle (null/absent = no typed fast + // paths, today's lowering exactly) and the module-wide stats the + // lowered functions accumulate into. the shape-guard lowering adds the shape + // telemetry: sites = atom property accesses that consulted the oracle, + // guards = shape diamonds emitted, declined = counted reasons + // (promotion criterion 5 — visible degradation). + oracle?: TypeOracle | null; + typed_stats?: { + diamonds: number; + trusted?: number; + shape_sites?: number; + shape_guards?: number; + // sites guarded with the 2-way polymorphic + // chain (a subset of shape_guards) + shape_poly_guards?: number; + shape_declined?: Record; + // born-with-shape telemetry — literal sites + // batched into make_object_shaped, constructor prefixes batched + // into fill_object_shaped diamonds, and counted fence declines + born_shaped?: number; + ctor_fills?: number; + fence_declined?: Record; + // typed (raw f64) slot accesses emitted + typed_loads?: number; + typed_stores?: number; + // construct sites virtualized by the optimizer's + // epoch-guarded constructor-result sinking + ctor_sunk?: number; + }; + // --types-dump: per-site shape census lines + shape_dump?: boolean; +} + +// clone-lowering mode (specialize.ts). The clone gets an +// unboxed signature (f64 formals, boxed once at entry); `trusted` +// selects how the body consumes the oracle: +// - trusted: oracle-number arithmetic lowers UNGUARDED — no diamonds, +// no slow paths. This is the deliberate unguarded-consumption +// line: oracle claims become facts, backed by the differential +// harness and by the escape analysis that restricts trusted clones +// to functions whose every runtime call the analysis covered. +// - untrusted (the export-boundary wrapper's clone, runtime-P2): the +// body keeps the ordinary guarded diamonds — the oracle is never +// consumed as fact, because the clone is entered from escaping +// entry points whose callers the analysis did NOT see (maam's +// constant-propagation domain may have pruned branches under +// call-site constants, so even all-number external arguments can +// escape its claims). The f64 formals are boxed once at entry; +// box_f64 is the optimizer's structural number proof, so +// formal-rooted diamonds fold trust-free. The diamond gate widens +// to assume-and-guard (see operandPlausiblyNumber). +export interface SpecMode { + cloneName: string; + trusted: boolean; + // formal parameter types; "f64" formals arrive raw and are boxed once + // at entry + formals: ("any" | "f64")[]; + // when "f64" (trusted clones only), `return ` with an + // oracle-number argument returns the raw f64 (unguarded unbox); any + // other return shape survives to the structural post-check in + // specialize.ts, which discards the clone + result: "any" | "f64"; +} + +// the runtime's shaped field-count ceiling +// (EJS_SHAPE_FIELD_CAP_MAX in runtime/ejs-shapes.h) — born-shaped sites +// beyond it would only ever take the runtime's sequential fallback, so +// they keep today's lowering +const EJS_SHAPE_FIELD_CAP_MAX = 14; + +// an environment-descriptor chain node: a per-iteration loop env or a +// function env (see envForBinding) +type EnvDesc = LoopEnv | FnInfo; + +interface ActiveLabel { + name: string; + breakBlock: Block; + continueBlock: Block | null; + ctxLen: number; +} + +interface FinallyCtx { + // the finalizer block; fresh copies lower at each crossing exit + node: e.BlockStatement; + breakDepth: number; + continueDepth: number; + handlerDepth: number; +} + +// the typed fast path: source operator -> low-tier f64 op +const f64ops: Record = { + "+": "f64_add", + "-": "f64_sub", + "*": "f64_mul", + "/": "f64_div", + "<": "f64_lt", +}; + +const binops: Record = { + "+": "add", + "-": "sub", + "*": "mul", + "/": "div", + "%": "mod", + "<": "lt", + "<=": "le", + ">": "gt", + ">=": "ge", + "==": "loose_eq", + "!=": "loose_neq", + "===": "strict_eq", + "!==": "strict_neq", + "&": "bitand", + "|": "bitor", + "^": "bitxor", + "<<": "shl", + ">>": "shr", + ">>>": "ushr", + instanceof: "instanceof", + in: "in", +}; + +// the source-level name a closure should carry (Function.prototype.name): +// the function\'s own id, or "" for anonymous functions — never the +// scope-qualified EIR name +function displayNameOf(childInfo: FnInfo): string { + return (childInfo.node.id && childInfo.node.id.name) || ""; +} + +class LowerFunction { + info: FnInfo; // FnInfo from scope analysis + analysis: ScopeAnalysis; + module: Module; + // toplevel-as-EIR: this function IS the module toplevel; import/ + // export statements lower here, and slot-backed declarations store + // through module slots instead of local bindings + isToplevel: boolean; + // module-scope interop: module-slot references (imports and this + // module's exports) + mod_ctx: ModCtx; + b: FunctionBuilder; + envParam: Inst; + thisParam: Inst; + // break/continue targets. loops push onto both stacks; switch + // statements only onto breakTargets (continue passes through a + // switch to the enclosing loop). + breakTargets: Block[] = []; + continueTargets: Block[] = []; + // labeled targets: LabeledStatement pushes loop labels onto + // pendingLabels; the loop lowering claims them (activeLabels) + // against its own exit/continue blocks. non-loop labels get a + // synthetic exit block. ctxLen = finallyCtx.length at label + // entry, so a labeled exit runs exactly the finalizers entered + // since the label. + pendingLabels: string[] = []; + activeLabels: ActiveLabel[] = []; + // materialized per-iteration loop envs lexically active at the + // current lowering position (innermost last). the current env + // value of each is tracked as a builder variable ("%loopenv#id"), + // so per-iteration refreshes flow through SSA/block params like + // any other variable (envs are ejsvals). + activeLoopEnvs: LoopEnv[] = []; + // active try/finally contexts. abrupt exits (return, break, + // continue) crossing a finally boundary lower a fresh copy of each + // crossed finalizer at the exit site (finalizer duplication). + finallyCtx: FinallyCtx[] = []; + curEnv: Inst; + // the module's type oracle (null = no typed fast paths) + oracle: TypeOracle | null; + // non-null when lowering a specialized clone + spec: SpecMode | null; + + constructor( + info: FnInfo, + analysis: ScopeAnalysis, + module: Module, + mod_ctx?: ModCtx, + spec?: SpecMode | null + ) { + this.info = info; + this.analysis = analysis; + this.module = module; + this.isToplevel = !!info.isToplevel; + this.mod_ctx = mod_ctx || { refs: new Map() }; + this.oracle = this.mod_ctx.oracle ?? null; + this.spec = spec ?? null; + + const paramNames = info.params.map((p) => p.uid); + this.b = new FunctionBuilder( + this.spec ? this.spec.cloneName : info.name, + ["%env", "%this"].concat(paramNames) + ); + this.envParam = this.b.fn.entry!.params[0]!; + this.thisParam = this.b.fn.entry!.params[1]!; + + // specialized-clone entry: f64 formals arrive raw and re-enter the + // boxed world exactly once, right here; the body then lowers + // against the boxed value like any other binding. (box_f64 is + // also the optimizer's value-intrinsic number proof, so any + // residual guarded diamond over a formal folds.) + if (this.spec) { + const entry = this.b.fn.entry!; + this.b.fn.sig = { formals: this.spec.formals.slice(), result: this.spec.result }; + for (let i = 0; i < info.params.length; i++) { + if (this.spec.formals[i] !== "f64") continue; + const p = entry.params[i + 2]!; + p.type = "f64"; + const boxed = this.b.emit("box_f64", [p], {}); + this.b.writeVariable(info.params[i]!.uid, entry, boxed); + } + } + // `this` reads go through the builder variable "%this" (seeded to + // the entry param by the builder): a derived constructor's super() + // call rebinds it (the runtime constructs the object and returns + // it), and SSA carries the update. for every other function it + // collapses to the entry param. + + // environment setup + this.curEnv = this.envParam; + if (info.envSize > 0) { + this.curEnv = this.b.emit("make_env", [], { size: info.envSize }); + if (info.parentSlot >= 0) + this.b.emit("env_store", [this.curEnv, this.envParam], { + slot: info.parentSlot, + }); + // captured parameters live in the env from function entry + for (let p of info.params) { + if (p.captured) { + let v = this.b.readVariable(p.uid, this.b.cur); + this.b.emit("env_store", [this.curEnv, v], { slot: p.slot }); + } + } + } + + // hoisted-var semantics: every local is readable (as undefined) + // from function entry, even before its declaration statement runs + // (`use(x); ... if (c) { var x = 5; }`). the declaration-time + // write in VariableDeclaration still handles the per-iteration + // reset of block-scoped lets in loops. loop-env bindings are + // skipped: their env doesn't exist yet (it's created at loop + // entry), and being let/const they're only visible inside the loop. + for (let binding of info.bindings) { + if (binding.loopEnv && binding.loopEnv.materialized) continue; + if (binding.kind === "local") this.writeBinding(binding, this.b.constUndefined()); + } + + // the arguments object, if referenced anywhere in this function + if (info.usesArguments) { + let a = this.b.emit("args_obj", [], {}); + this.writeBinding(info.argumentsBinding!, a); + } + + // an arrow below captures our `this`: store it in the env (kept + // in sync by intrinsicCall when super() rebinds this) + if (info.thisBinding && info.thisBinding.captured) + this.writeBinding(info.thisBinding, this.thisParam); + + // the rest parameter materializes from the trailing arguments + if (info.restBinding) { + let rest = this.b.emit("rest_args", [], { index: info.params.length }); + this.writeBinding(info.restBinding, rest); + } + + // default parameters: a param that arrived undefined takes its + // default (evaluated left to right, in the function scope). the + // conditional write merges via SSA (or the env, for captured + // params, whose initial store just happened above). + let defaults = info.defaults || []; + let ndefaults = Math.min(defaults.length, info.params.length); + for (let i = 0; i < ndefaults; i++) { + const dflt = defaults[i]; + if (!dflt) continue; + const pb = info.params[i]!; + const cur = this.readBinding(pb); + let isundef = this.b.emit("strict_eq", [cur, this.b.constUndefined()], {}); + let ubool = this.b.emit("to_boolean", [isundef], {}); + let dflt_bb = this.b.newBlock(`default_${pb.name}`); + let join_bb = this.b.newBlock(`default_join_${pb.name}`); + this.b.condBr(ubool, dflt_bb, [], join_bb, []); + this.b.sealBlock(dflt_bb); + this.b.setInsertPoint(dflt_bb); + const dv = this.expr(dflt); + this.writeBinding(pb, dv); + this.b.br(join_bb, []); + this.b.sealBlock(join_bb); + this.b.setInsertPoint(join_bb); + } + + // hoist function declarations: their closures exist from entry + for (let binding of info.bindings) { + if (binding.kind === "fn") { + let childInfo = this.findChildFn(binding); + let closure = this.b.emit("make_closure", [this.curEnv], { + fn: childInfo.name, + name: displayNameOf(childInfo), + }); + this.writeBinding(binding, closure); + } + } + } + + findChildFn(binding: Binding): FnInfo { + for (let c of this.info.children) { + if (c.node.id && c.node.id.name === binding.name) return c; + } + throw new Error(`EIR lowering: no child function for binding ${binding.name}`); + } + + // --- binding access ----------------------------------------------------------- + + // Environments form a chain of descriptors: per-iteration loop envs + // (LoopEnv, parent in slot 0) inside their function's env (FnInfo, + // parent in parentSlot), which chains to the descriptor current at + // the function's definition site. envForBinding walks that chain + // from the current lowering position to the descriptor holding the + // binding, emitting one env_load per hop. + + levar(le: LoopEnv): string { + return `%loopenv#${le.id}`; + } + + // the env value make_closure should capture at the current position + curEnvValue(): Inst { + if (this.activeLoopEnvs.length > 0) { + const le = this.activeLoopEnvs[this.activeLoopEnvs.length - 1]!; + return this.b.readVariable(this.levar(le), this.b.cur); + } + return this.curEnv; + } + + // the innermost materialized descriptor at f's definition site + descAtCreation(f: FnInfo): EnvDesc | null { + let le = f.creationLoopEnv; + while (le && !le.materialized) le = le.parentCandidate; + if (le) return le; + let p = f.parent; + if (!p) return null; + if (p.envSize > 0) return p; + return this.descAtCreation(p); + } + + // the descriptor whose env value lives in desc's parent slot + parentDescOf(desc: EnvDesc): EnvDesc | null { + if (desc.isLoopEnv) { + // slot 0 holds curEnv at loop entry: the nearest enclosing + // materialized loop env, else the function env, else the + // function's creation-site descriptor (== its incoming env) + let le = desc.parentCandidate; + while (le && !le.materialized) le = le.parentCandidate; + if (le) return le; + if (desc.fnInfo!.envSize > 0) return desc.fnInfo!; + return this.descAtCreation(desc.fnInfo!); + } + // a function env's parent slot holds its incoming env + return this.descAtCreation(desc); + } + + // fresh per-iteration env for captured let/const declared in the loop + // BODY: emitted at the top of the body block each iteration. their + // declarations re-execute per pass, so nothing copies forward. + enterLoopBody(n: e.Node): LoopEnv | null { + let ble = this.analysis.loopBodyEnvOf(n); + if (!ble) return null; + let outer = this.curEnvValue(); + let e = this.b.emit("make_env", [], { size: ble.envSize }); + this.b.emit("env_store", [e, outer], { slot: 0 }); + this.b.writeVariable(this.levar(ble), this.b.cur, e); + this.activeLoopEnvs.push(ble); + return ble; + } + + leaveLoopBody(ble: LoopEnv | null): void { + if (ble) this.activeLoopEnvs.pop(); + } + + // a loop lowering claims any labels the enclosing LabeledStatement(s) + // queued, binding them to its own break/continue blocks + claimPendingLabels(breakBlock: Block, continueBlock: Block | null): number { + let n = this.pendingLabels.length; + for (let name of this.pendingLabels) + this.activeLabels.push({ + name: name, + breakBlock: breakBlock, + continueBlock: continueBlock, + ctxLen: this.finallyCtx.length, + }); + this.pendingLabels = []; + return n; + } + + releaseLabels(n: number): void { + while (n-- > 0) this.activeLabels.pop(); + } + + findLabel(name: string, loc: e.SourceLocation | null | undefined): ActiveLabel { + for (let i = this.activeLabels.length - 1; i >= 0; i--) + if (this.activeLabels[i]!.name === name) return this.activeLabels[i]!; + throw LowerNotSupported(`unknown label '${name}'`, loc); + } + + // the environment holding `binding`, from the current position + envForBinding(binding: Binding): Inst { + let target = + binding.loopEnv && binding.loopEnv.materialized ? binding.loopEnv : binding.fnInfo; + + let desc: EnvDesc | null; + let env: Inst; + if (this.activeLoopEnvs.length > 0) { + const top = this.activeLoopEnvs[this.activeLoopEnvs.length - 1]!; + desc = top; + env = this.b.readVariable(this.levar(top), this.b.cur); + } else if (this.info.envSize > 0) { + desc = this.info; + env = this.curEnv; + } else { + desc = this.descAtCreation(this.info); + env = this.envParam; + } + + while (desc && desc !== target) { + let slot = desc.isLoopEnv ? 0 : desc.parentSlot; + if (slot < 0) + throw new Error( + `EIR lowering: broken env chain through ${desc.isLoopEnv ? `loopenv#${desc.id}` : desc.name}` + ); + env = this.b.emit("env_load", [env], { slot: slot }); + desc = this.parentDescOf(desc); + } + if (!desc) throw new Error(`EIR lowering: env chain missed ${binding.uid}`); + return env; + } + + readBinding(binding: Binding): Inst { + if (!binding.captured) return this.b.readVariable(binding.uid, this.b.cur); + let env = this.envForBinding(binding); + return this.b.emit("env_load", [env], { slot: binding.slot }); + } + + writeBinding(binding: Binding, value: Inst): void { + if (!binding.captured) { + this.b.writeVariable(binding.uid, this.b.cur, value); + return; + } + let env = this.envForBinding(binding); + this.b.emit("env_store", [env, value], { slot: binding.slot }); + } + + // --- expressions ---------------------------------------------------------- + + expr(n: e.Expression | e.SpreadElement): Inst { + switch (n.type) { + case "Literal": + return this.literal(n); + case "Identifier": + return this.identifier(n); + case "ThisExpression": { + // resolved to a binding = an arrow's lexical this (the + // owner's captured this, read through the env chain) + let binding = this.analysis.resolve(n); + if (binding) return this.readBinding(binding); + return this.b.readVariable("%this", this.b.cur); + } + case "BinaryExpression": + return this.binary(n); + case "LogicalExpression": + return this.logical(n); + case "UnaryExpression": + return this.unary(n); + case "AssignmentExpression": + return this.assignment(n); + case "UpdateExpression": + return this.update(n); + case "TemplateLiteral": + return this.template(n); + case "TaggedTemplateExpression": + return this.taggedTemplate(n); + case "CallExpression": + return this.call(n); + case "NewExpression": + return this.newExpr(n); + case "MemberExpression": + return this.member(n); + case "ConditionalExpression": + return this.conditional(n); + case "FunctionExpression": + case "ArrowFunctionExpression": + return this.functionExpr(n); + case "SequenceExpression": { + let v: Inst | undefined; + for (const sub of n.expressions) v = this.expr(sub); + return v!; + } + case "ArrayExpression": { + // holes must stay holes (forEach etc. skip them; undefined + // wouldn't be skipped). written with plain loops: the + // arrow-based form of this case miscompiled under the + // legacy pipeline (undistilled; see the phase-3 notes). + let holes = false; + for (let el of n.elements) if (!el) holes = true; + if (!holes) { + const elems: Inst[] = []; + for (const el of n.elements) elems.push(this.expr(el!)); + return this.b.emit("make_array", elems, {}); + } + let vals = []; + let indices = []; + for (let i = 0; i < n.elements.length; i++) { + let el = n.elements[i]; + if (!el) continue; + vals.push(this.expr(el)); + indices.push(i); + } + return this.b.emit("make_array", vals, { + len: n.elements.length, + indices: indices, + }); + } + case "ObjectExpression": { + let hasAccessors = n.properties.some((p) => p.kind && p.kind !== "init"); + if (hasAccessors) return this.objectWithAccessors(n); + let hasComputed = n.properties.some( + (p) => p.computed || (p.key.type !== "Identifier" && p.key.type !== "Literal") + ); + let hasProto = n.properties.some((p) => this.isProtoProp(p)); + if (!hasComputed && !hasProto) { + const keys: string[] = []; + const values: Inst[] = []; + for (const p of n.properties) { + keys.push( + p.key.type === "Identifier" + ? p.key.name + : String((p.key as e.Literal).value) + ); + values.push(this.expr(p.value as e.Expression)); + } + // a statically-keyed literal is born + // with its shape — key order and count are the site's + // static truth, no oracle fact needed (the runtime + // derives true reprs from the actual values and falls + // back to sequential sets off the shaped fast path). + // NOT --types-gated (gc-P5): without the oracle the + // static reprs are simply all-boxed; the runtime's + // birth derivation supplies the true ones, and the + // single-cell embedded allocation applies to flag-off + // literals exactly as to typed ones. + if ( + passes().bornShaped && + keys.length >= 1 && + keys.length <= EJS_SHAPE_FIELD_CAP_MAX && + new Set(keys).size === keys.length && + keys.every((k) => !/^[0-9]/.test(k)) + ) { + const fields: ShapeField[] = n.properties.map((p, i) => ({ + name: keys[i]!, + repr: this.operandIsNumber(p.value as e.Expression) + ? ("f64" as const) + : ("boxed" as const), + })); + const key = this.module.internShape(fields); + const stats = this.mod_ctx.typed_stats; + if (stats) stats.born_shaped = (stats.born_shaped ?? 0) + 1; + return this.b.emit("make_object_shaped", values, { shape: key }); + } + return this.b.emit("make_object", values, { keys: keys }); + } + // computed keys or a `__proto__:` definition: empty object + // + per-property stores in source order (key evaluates + // before value, per spec) + let obj = this.b.emit("make_object", [], { keys: [] }); + for (let p of n.properties) { + if (this.isProtoProp(p)) { + const v = this.expr(p.value as e.Expression); + this.b.emit("call_runtime", [obj, v], { + name: "object_literal_set_proto", + }); + } else if (!p.computed && (p.key.type === "Identifier" || p.key.type === "Literal")) { + const v = this.expr(p.value as e.Expression); + this.b.emit("set_prop_atom", [obj, v], { + atom: p.key.type === "Identifier" ? p.key.name : String((p.key as e.Literal).value), + }); + } else { + let k = this.expr(p.key); + const v = this.expr(p.value as e.Expression); + this.b.emit("set_prop", [obj, k, v], {}); + } + } + return obj; + } + default: + throw LowerNotSupported(`expression type ${n.type}`, n.loc); + } + } + + // `__proto__: expr` in an object literal (non-computed, non-method, + // non-shorthand, string or identifier key) is a prototype definition, + // not an own property (B.3.1 / PropertyDefinitionEvaluation) + isProtoProp(p: e.Property): boolean { + if (p.computed || p.method || p.shorthand) return false; + if (p.kind && p.kind !== "init") return false; + if (p.key.type === "Identifier") return p.key.name === "__proto__"; + return p.key.type === "Literal" && p.key.value === "__proto__"; + } + + // an object literal containing get/set accessors: empty object, then + // per-property defines in source order. a non-computed get/set PAIR + // for one name becomes a single define_accessor (name-keyed — keying + // by the key AST node is how the class desugar lost getters, bug + // #14). computed-key accessors each define separately in source + // order (their keys are distinct evaluations); the runtime merges + // the partial descriptors. + objectWithAccessors(n: e.ObjectExpression): Inst { + let obj = this.b.emit("make_object", [], { keys: [] }); + const done = new Set(); + for (let i = 0; i < n.properties.length; i++) { + const p = n.properties[i]!; + if (p.computed) { + let key = this.expr(p.key); + if (p.kind && p.kind !== "init") { + const accessor = this.expr(p.value as e.Expression); + this.b.emit("define_accessor_computed", [obj, key, accessor], { + kind: p.kind, + }); + } else { + const v = this.expr(p.value as e.Expression); + this.b.emit("set_prop", [obj, key, v], {}); + } + continue; + } + if (p.key.type !== "Identifier" && p.key.type !== "Literal") + throw LowerNotSupported(`accessor object literal key ${p.key.type}`, n.loc); + if (this.isProtoProp(p)) { + const v = this.expr(p.value as e.Expression); + this.b.emit("call_runtime", [obj, v], { name: "object_literal_set_proto" }); + continue; + } + const name = p.key.type === "Identifier" ? p.key.name : String((p.key as e.Literal).value); + if (p.kind && p.kind !== "init") { + if (done.has(name)) continue; // the pair lowered together + done.add(name); + let getter: Inst | null = null; + let setter: Inst | null = null; + for (let j = i; j < n.properties.length; j++) { + const q = n.properties[j]!; + if (q.kind === "init" || q.computed) continue; + const qname = q.key.type === "Identifier" ? q.key.name : String((q.key as e.Literal).value); + if (qname !== name) continue; + if (q.kind === "get") getter = this.expr(q.value as e.Expression); + else if (q.kind === "set") setter = this.expr(q.value as e.Expression); + } + this.b.emit( + "define_accessor", + [obj, getter || this.b.constUndefined(), setter || this.b.constUndefined()], + { atom: name } + ); + } else { + const v = this.expr(p.value as e.Expression); + this.b.emit("set_prop_atom", [obj, v], { atom: name }); + } + } + return obj; + } + + literal(n: e.Literal): Inst { + if (n.value === null) return this.b.constNull(); + switch (typeof n.value) { + case "number": + return this.b.constNumber(n.value); + case "string": + return this.b.constAtom(n.value); + case "boolean": + return this.b.constBool(n.value); + case "object": { + // a regex literal: fresh RegExp per evaluation, like the + // legacy visitLiteral + if (typeof n.value.source !== "string") + throw LowerNotSupported(`literal ${typeof n.value}`, n.loc); + let flags = + (n.value.global ? "g" : "") + + (n.value.multiline ? "m" : "") + + (n.value.ignoreCase ? "i" : "") + + (n.value.sticky ? "y" : "") + + (n.value.unicode ? "u" : ""); + return this.b.emit("make_regexp", [], { + source: n.value.source, + flags: flags, + }); + } + default: + throw LowerNotSupported(`literal ${typeof n.value}`, n.loc); + } + } + + identifier(n: e.Identifier): Inst { + if (n.name === "undefined") return this.b.constUndefined(); + let binding = this.analysis.resolve(n); + if (binding === null || binding === undefined) { + let ref = this.mod_ctx.refs.get(n.name); + if (ref) { + if (ref.exotic !== undefined) + return this.b.emit("module_get_exotic", [], { module: ref.exotic }); + if (ref.constval !== undefined) return this.literal(ref.constval); + return this.b.emit("module_slot_load", [], { + module: ref.module, + slot: ref.slot, + }); + } + return this.b.emit("get_global", [], { atom: n.name }); + } + if (binding.kind === "self") + throw LowerNotSupported("function self-reference as a value", n.loc); + return this.readBinding(binding); + } + + functionExpr(n: e.FunctionExpression | e.ArrowFunctionExpression): Inst { + let childInfo = this.analysis.infoFor(n); + if (!childInfo) throw new Error("EIR lowering: unanalyzed function expression"); + lowerOneFunction(childInfo, this.analysis, this.module, this.mod_ctx); + // capture the innermost env: the current iteration's loop env when + // inside a for-let loop, else the function env / incoming env + return this.b.emit("make_closure", [this.curEnvValue()], { + fn: childInfo.name, + name: displayNameOf(childInfo), + }); + } + + binary(n: e.BinaryExpression): Inst { + let op = binops[n.operator]; + if (!op) throw LowerNotSupported(`binary operator ${n.operator}`, n.loc); + let l = this.expr(n.left); + let r = this.expr(n.right); + // born-typed guarded arithmetic. When the oracle types + // BOTH operands as exactly {number}, split the same diamond shape + // logical() uses: has_tag guards -> fast unbox/f64 op/box vs the + // generic slow op, rejoining in a boxed block param. Guarded + // consumption is correct even when the oracle is wrong — the + // has_tag guards decide at runtime; only code size/speed change. + const f64op = f64ops[n.operator]; + if (f64op && this.operandIsNumber(n.left) && this.operandIsNumber(n.right)) { + // trusted-clone bodies consume the oracle UNGUARDED: no + // diamond, no slow path — unbox, compute, re-box. Everywhere + // else the guarded diamond stands. + if (this.spec && this.spec.trusted) return this.trustedNumeric(f64op, l, r); + return this.numericDiamond(f64op, op, l, r); + } + // untrusted (wrapper) clone bodies assume-and-guard: the diamond + // is correct for ANY operand values, so a plausibly-number claim + // (not provably non-number — incl. nodes the oracle never saw, + // the norm for an exported-but-never-called-internally function) + // is enough to justify emitting it. The entry box_f64 proofs + // fold the formal-rooted ones; the rest keep their slow paths. + if ( + f64op && + this.spec && + !this.spec.trusted && + this.operandPlausiblyNumber(n.left) && + this.operandPlausiblyNumber(n.right) + ) + return this.numericDiamond(f64op, op, l, r); + return this.b.emit(op, [l, r], {}); + } + + // unguarded typed arithmetic (clone lowering only): unbox both + // operands, apply the f64 op, and re-enter the boxed world. f64_lt's + // i1 rejoins as boxed booleans through the same constant-edge shape + // the diamond's fast arm uses (i1 never crosses a block boundary). + trustedNumeric(f64op: string, l: Inst, r: Inst): Inst { + if (this.mod_ctx.typed_stats) + this.mod_ctx.typed_stats.trusted = (this.mod_ctx.typed_stats.trusted ?? 0) + 1; + const ua = this.b.emit("unbox_f64", [l], {}); + const ub = this.b.emit("unbox_f64", [r], {}); + const v = this.b.emit(f64op, [ua, ub], {}); + if (f64op !== "f64_lt") return this.b.emit("box_f64", [v], {}); + const t_bb = this.b.newBlock("trust_lt_true"); + const f_bb = this.b.newBlock("trust_lt_false"); + const join_bb = this.b.newBlock("trust_lt_join"); + const result = join_bb.addParam("lt"); + this.b.condBr(v, t_bb, [], f_bb, []); + this.b.sealBlock(t_bb); + this.b.sealBlock(f_bb); + this.b.setInsertPoint(t_bb); + this.b.br(join_bb, [this.b.constBool(true)]); + this.b.setInsertPoint(f_bb); + this.b.br(join_bb, [this.b.constBool(false)]); + this.b.sealBlock(join_bb); + this.b.setInsertPoint(join_bb); + return result; + } + + // Does this operand node type as exactly {number}? Numeric literals + // qualify directly: the oracle's mapping policy leaves literals + // unmapped (glue), so `x + 1` would otherwise never take the fast + // path. (A unary +/- on a numeric literal is the parsed form of a + // signed literal — pre-EIR desugar does not fold it.) Everything + // else asks the oracle, and only a pure {number} answer qualifies — + // not top, and not reassignment-widened unions like number|undefined. + operandIsNumber(node: e.Expression): boolean { + if (!this.oracle) return false; // no oracle, no diamonds — today's lowering + if (node.type === "Literal") return typeof node.value === "number"; + if ( + node.type === "UnaryExpression" && + (node.operator === "-" || node.operator === "+") && + node.argument.type === "Literal" && + typeof (node.argument as e.Literal).value === "number" + ) + return true; + const t = this.oracle.typeOfNode(node); + return t.tags !== "top" && t.tags.size === 1 && t.tags.has("number"); + } + + // Could this operand be a number at runtime? The permissive twin of + // operandIsNumber, for untrusted-clone bodies only: a diamond's guard + // decides at runtime, so the only reason NOT to emit one is a proof + // it can never pass — a non-numeric literal, or an oracle answer that + // positively excludes number. top/unmapped nodes assume-and-guard. + operandPlausiblyNumber(node: e.Expression): boolean { + if (node.type === "Literal") return typeof node.value === "number"; + if ( + node.type === "UnaryExpression" && + (node.operator === "-" || node.operator === "+") && + node.argument.type === "Literal" + ) + return typeof (node.argument as e.Literal).value === "number"; + if (!this.oracle) return true; + const t = this.oracle.typeOfNode(node); + return t.tags === "top" || t.tags.has("number"); + } + + // has_tag(l) -> has_tag(r) -> fast: unbox both, f64 op, rejoin boxed; + // any guard failure -> slow: the generic op. The join param is an + // ejsval: raw f64/i1 never crosses a block boundary (P2 verifier + // rule), so f64 results re-box in the fast block and f64_lt's i1 + // branches to boolean-constant edges into the join. + numericDiamond(f64op: string, genericOp: string, l: Inst, r: Inst): Inst { + if (this.mod_ctx.typed_stats) this.mod_ctx.typed_stats.diamonds++; + + const guard2_bb = this.b.newBlock("num_guard2"); + const fast_bb = this.b.newBlock("num_fast"); + const slow_bb = this.b.newBlock("num_slow"); + const join_bb = this.b.newBlock("num_join"); + const result = join_bb.addParam("num"); + + const t1 = this.b.emit("has_tag", [l], { tag: "number" }); + this.b.condBr(t1, guard2_bb, [], slow_bb, []); + this.b.sealBlock(guard2_bb); + + this.b.setInsertPoint(guard2_bb); + const t2 = this.b.emit("has_tag", [r], { tag: "number" }); + this.b.condBr(t2, fast_bb, [], slow_bb, []); + this.b.sealBlock(fast_bb); + this.b.sealBlock(slow_bb); + + this.b.setInsertPoint(fast_bb); + const ua = this.b.emit("unbox_f64", [l], {}); + const ub = this.b.emit("unbox_f64", [r], {}); + const v = this.b.emit(f64op, [ua, ub], {}); + if (f64op === "f64_lt") { + const t_bb = this.b.newBlock("num_lt_true"); + const f_bb = this.b.newBlock("num_lt_false"); + this.b.condBr(v, t_bb, [], f_bb, []); + this.b.sealBlock(t_bb); + this.b.sealBlock(f_bb); + this.b.setInsertPoint(t_bb); + this.b.br(join_bb, [this.b.constBool(true)]); + this.b.setInsertPoint(f_bb); + this.b.br(join_bb, [this.b.constBool(false)]); + } else { + const boxed = this.b.emit("box_f64", [v], {}); + this.b.br(join_bb, [boxed]); + } + + this.b.setInsertPoint(slow_bb); + const g = this.b.emit(genericOp, [l, r], {}); + this.b.br(join_bb, [g]); + this.b.sealBlock(join_bb); + + this.b.setInsertPoint(join_bb); + return result; + } + + // --- shape-guarded property access --------------------- + // + // The promotion policy (criteria 1/2 of the plan): a diamond is emitted + // only for an EXACT receiver-shape fact — monomorphic, non-megamorphic, + // uncapped, ordered witness present, every field's repr a single tag, + // and the accessed field actually in the shape. Anything less is a + // counted decline and today's generic op. Guarded consumption is + // correct even when the oracle is wrong: the has_shape compare decides + // at runtime, and a failed guard costs speed, never behavior. + // -fno-shape-guards is the compile-time bisect hook (the + // -fno-eir-opt mold); runtime EJS_SHAPES=off makes every guard fail. + + shapeDecline(reason: string): null { + const stats = this.mod_ctx.typed_stats; + if (stats) { + const d = (stats.shape_declined ??= {}); + d[reason] = (d[reason] ?? 0) + 1; + } + return null; + } + + // --types-dump: one census line per consulted access site + shapeDumpSite(objNode: e.Expression, atom: string, what: string): void { + if (!this.mod_ctx.shape_dump) return; + const loc = (objNode as { loc?: { start?: { line: number; column: number } } }).loc; + const where = loc && loc.start ? `${loc.start.line}:${loc.start.column + 1}` : "synthetic"; + console.warn(`--types-dump: shapes: .${atom} @${where}: ${what}`); + } + + // the exact shape facts for accessing `atom` on the value of `objNode` + // — one fact per oracle shape (two = the polymorphic chain), or + // null (with the decline counted) when anything is short of exact. + // Every shape in a multi-shape answer must carry the field: a shape + // that lacks it would need the fast arm to run proto-lookup semantics, + // which only the generic path performs (criterion 2 — no near-misses). + // -fno-poly-shape-guards bisects polymorphic chains: 2-shape sites + // decline "polymorphic" exactly as they did before the extension. + shapeFactFor( + objNode: e.Expression | null, + atom: string + ): { key: string; slot: number; repr: "boxed" | "f64" }[] | null { + if (!objNode || !this.oracle || !this.oracle.receiverShapeOfNode) return null; + if (!passes().shapeGuards) return null; + const stats = this.mod_ctx.typed_stats; + if (stats) stats.shape_sites = (stats.shape_sites ?? 0) + 1; + const q = this.oracle.receiverShapeOfNode(objNode); + if (q.declined !== undefined) { + this.shapeDumpSite(objNode, atom, `declined ${q.declined}`); + return this.shapeDecline(q.declined); + } + if (q.shapes.length > 1 && !passes().polyShapeGuards) { + this.shapeDumpSite(objNode, atom, "declined polymorphic"); + return this.shapeDecline("polymorphic"); + } + const facts: { key: string; slot: number; repr: "boxed" | "f64" }[] = []; + for (const fields of q.shapes) { + const slot = fields.findIndex((f) => f.name === atom); + if (slot < 0) { + this.shapeDumpSite(objNode, atom, "declined no-field"); + return this.shapeDecline("no-field"); // proto/method access + } + const key = this.module.internShape(fields); + // structurally-equal shapes reported twice guard once + if (!facts.some((f) => f.key === key)) + facts.push({ key, slot, repr: fields[slot]!.repr }); + } + if (facts.length === 0) return this.shapeDecline("unmapped"); + if (stats) { + stats.shape_guards = (stats.shape_guards ?? 0) + 1; + if (facts.length > 1) + stats.shape_poly_guards = (stats.shape_poly_guards ?? 0) + 1; + } + this.shapeDumpSite( + objNode, + atom, + facts.map((f) => `guarded shape="${f.key}" slot=${f.slot}`).join(" | ") + ); + return facts; + } + + // obj.atom: a has_shape chain whose fast arms are fixed-slot loads and + // whose shared slow arm is today's generic get — the numericDiamond + // skeleton with one guard per exact fact. One fact is the mono + // diamond exactly; two facts (the polymorphic extension) test the + // second shape on the first guard's miss edge, so each fast arm sits + // under its own same-block-fresh has_shape fact and the verifier's + // rules apply per arm unchanged. + propGet(objNode: e.Expression | null, obj: Inst, atom: string): Inst { + const facts = this.shapeFactFor(objNode, atom); + if (!facts) return this.b.emit("get_prop_atom", [obj], { atom: atom }); + + const fast_bbs = facts.map(() => this.b.newBlock("shape_fast")); + const chk_bbs = facts.slice(1).map(() => this.b.newBlock("shape_chk")); + const slow_bb = this.b.newBlock("shape_slow"); + const join_bb = this.b.newBlock("shape_join"); + const result = join_bb.addParam("prop"); + + for (let i = 0; i < facts.length; i++) { + const f = facts[i]!; + const miss = i + 1 < facts.length ? chk_bbs[i]! : slow_bb; + const t = this.b.emit("has_shape", [obj], { shape: f.key }); + this.b.condBr(t, fast_bbs[i]!, [], miss, []); + this.b.sealBlock(fast_bbs[i]!); + this.b.sealBlock(miss); + if (miss !== slow_bb) this.b.setInsertPoint(miss); + } + + for (let i = 0; i < facts.length; i++) { + const f = facts[i]!; + this.b.setInsertPoint(fast_bbs[i]!); + const v = this.b.emit("slot_load", [obj], { shape: f.key, slot: f.slot, repr: f.repr }); + if (f.repr === "f64") { + // typed slots: the load produces a raw f64 (the guard + // proved the repr; the slot bytes ARE the double). Box once at + // the fast exit — the join stays boxed (its slow edge is the + // generic get), and the optimizer's region fusion + rawJoin + // machinery strips the box wherever the consumer is raw. + v.type = "f64"; + const stats = this.mod_ctx.typed_stats; + if (stats) stats.typed_loads = (stats.typed_loads ?? 0) + 1; + const boxed = this.b.emit("box_f64", [v], {}); + this.b.br(join_bb, [boxed]); + } else { + this.b.br(join_bb, [v]); + } + } + + this.b.setInsertPoint(slow_bb); + const g = this.b.emit("get_prop_atom", [obj], { atom: atom }); + this.b.br(join_bb, [g]); + this.b.sealBlock(join_bb); + + this.b.setInsertPoint(join_bb); + return result; + } + + // obj.atom = v: the store dual. The fast arm must prove the stored + // value's runtime repr matches the field's shape repr (a mismatched + // store owes a shape TRANSITION, which only the generic path performs), + // so the guard is has_shape AND a has_tag(number) check oriented by the + // field repr — f64 fields take numbers fast, boxed fields take + // non-numbers fast, everything else goes generic. + propSet(objNode: e.Expression | null, obj: Inst, atom: string, v: Inst): void { + const facts = this.shapeFactFor(objNode, atom); + if (!facts) { + this.b.emit("set_prop_atom", [obj, v], { atom: atom }); + return; + } + + // per-fact tag+fast pair (mono creation order preserved: tag, + // fast, slow, join), then the chain blocks + const tag_bbs = facts.map(() => this.b.newBlock("shape_settag")); + const fast_bbs = facts.map(() => this.b.newBlock("shape_setfast")); + const chk_bbs = facts.slice(1).map(() => this.b.newBlock("shape_setchk")); + const slow_bb = this.b.newBlock("shape_setslow"); + const join_bb = this.b.newBlock("shape_setjoin"); + + for (let i = 0; i < facts.length; i++) { + const f = facts[i]!; + const miss = i + 1 < facts.length ? chk_bbs[i]! : slow_bb; + const t = this.b.emit("has_shape", [obj], { shape: f.key }); + this.b.condBr(t, tag_bbs[i]!, [], miss, []); + this.b.sealBlock(tag_bbs[i]!); + if (miss !== slow_bb) this.b.sealBlock(miss); + + this.b.setInsertPoint(tag_bbs[i]!); + const isnum = this.b.emit("has_tag", [v], { tag: "number" }); + if (f.repr === "f64") this.b.condBr(isnum, fast_bbs[i]!, [], slow_bb, []); + else this.b.condBr(isnum, slow_bb, [], fast_bbs[i]!, []); + this.b.sealBlock(fast_bbs[i]!); + // slow's predecessors: every tag block plus the last miss edge + if (i === facts.length - 1) this.b.sealBlock(slow_bb); + if (i + 1 < facts.length) this.b.setInsertPoint(chk_bbs[i]!); + } + + for (let i = 0; i < facts.length; i++) { + const f = facts[i]!; + this.b.setInsertPoint(fast_bbs[i]!); + if (f.repr === "f64") { + // typed slots: unbox under the has_tag guard (the true + // edge into this block proved v is a number, so the bits are + // the double) and store raw — the type system carries the + // repr proof the verifier's store rule now requires. + const raw = this.b.emit("unbox_f64", [v], {}); + this.b.emit("slot_store", [obj, raw], { shape: f.key, slot: f.slot, repr: f.repr }); + const stats = this.mod_ctx.typed_stats; + if (stats) stats.typed_stores = (stats.typed_stores ?? 0) + 1; + } else { + this.b.emit("slot_store", [obj, v], { shape: f.key, slot: f.slot, repr: f.repr }); + } + this.b.br(join_bb, []); + } + + this.b.setInsertPoint(slow_bb); + this.b.emit("set_prop_atom", [obj, v], { atom: atom }); + this.b.br(join_bb, []); + this.b.sealBlock(join_bb); + + this.b.setInsertPoint(join_bb); + } + + // --- the fenced constructor prefix --------------------- + // + // Detect the maximal leading run of `this. = ` + // statements in a plain function body and batch it into ONE guarded + // fill_object_shaped diamond. The fence is structural and oracle-free + // (the specialization discipline — a lying oracle cannot make this wrong): + // + // - plain function, not an arrow (whose `this` is lexical), not the + // toplevel, not a specialization clone; + // - every stored value is a Literal or an Identifier resolving to a + // local binding — evaluating it cannot run user code, so hoisting + // the evaluations above the batched stores is observably identical; + // - nothing else appears between the stores (they are consecutive + // statements), so no code can observe the receiver mid-prefix — + // `"y" in this` between stores, an escaping call, a getter-running + // value all CUT the prefix at that statement; + // - names distinct, non-index-looking, not __proto__, count within + // the runtime's shaped field cap. + // + // The batching is additionally guarded at runtime by has_shape(this, "") + // — only a construct-fresh EMPTY receiver takes the fast arm; a reused + // this (F.call(o)), a dictionary-mode object, or EJS_SHAPES=off all + // fail the one-compare guard and run the original sequential stores. + // The runtime call re-checks everything again (incl. proto-chain + // accessor interception) and falls back to sequential [[Set]]s, so a + // wrong guard can cost speed, never behavior. -fno-born-shaped is + // the bisect hook. Returns how many leading statements were consumed. + + fenceDecline(reason: string): void { + const stats = this.mod_ctx.typed_stats; + if (stats) { + const d = (stats.fence_declined ??= {}); + d[reason] = (d[reason] ?? 0) + 1; + } + } + + lowerBornShapedCtorPrefix(body: e.BlockStatement): number { + if (!this.oracle || !passes().bornShaped) return 0; + if (this.isToplevel || this.spec) return 0; + if (this.info.node.type === "ArrowFunctionExpression") return 0; + + const names: string[] = []; + const valueNodes: e.Expression[] = []; + let cutReason: string | null = null; + for (const s of body.body) { + const cut = (why: string): true => ((cutReason = why), true); + if (s.type !== "ExpressionStatement") break; + const a = s.expression; + if (a.type !== "AssignmentExpression" || a.operator !== "=") break; + const m = a.left; + if (m.type !== "MemberExpression" || m.computed) break; + if (m.object.type !== "ThisExpression") break; + if (m.property.type !== "Identifier") break; + const name = m.property.name; + if (name === "__proto__" || /^[0-9]/.test(name)) { + cut("unshapeable-name"); + break; + } + if (names.includes(name)) { + cut("duplicate-name"); + break; + } + const v = a.right; + if (v.type !== "Literal" && !(v.type === "Identifier" && this.analysis.resolve(v))) { + cut("value-not-local"); + break; + } + names.push(name); + valueNodes.push(v); + } + if (names.length < 2) { + // a ctor-looking body (at least one conforming this-store) that + // did not reach the batching threshold is a counted decline; + // everything else simply is not a constructor prefix + if (names.length === 1) this.fenceDecline(cutReason ?? "short-prefix"); + return 0; + } + if (names.length > EJS_SHAPE_FIELD_CAP_MAX) { + this.fenceDecline("capped"); + return 0; + } + + // values first (locals/literals — effect-free), then the guard + const values = valueNodes.map((v) => this.expr(v)); + const fields: ShapeField[] = names.map((name, i) => ({ + name, + repr: this.operandIsNumber(valueNodes[i]!) ? ("f64" as const) : ("boxed" as const), + })); + const key = this.module.internShape(fields); + this.module.internShape([]); // the guard's empty shape + const thisVal = this.b.readVariable("%this", this.b.cur); + + const fast_bb = this.b.newBlock("ctor_fill_fast"); + const slow_bb = this.b.newBlock("ctor_fill_slow"); + const join_bb = this.b.newBlock("ctor_fill_join"); + const t = this.b.emit("has_shape", [thisVal], { shape: "" }); + this.b.condBr(t, fast_bb, [], slow_bb, []); + this.b.sealBlock(fast_bb); + this.b.sealBlock(slow_bb); + + this.b.setInsertPoint(fast_bb); + this.b.emit("fill_object_shaped", [thisVal, ...values], { shape: key }); + this.b.br(join_bb, []); + + this.b.setInsertPoint(slow_bb); + for (let i = 0; i < names.length; i++) + this.b.emit("set_prop_atom", [thisVal, values[i]!], { atom: names[i]! }); + this.b.br(join_bb, []); + this.b.sealBlock(join_bb); + this.b.setInsertPoint(join_bb); + + const stats = this.mod_ctx.typed_stats; + if (stats) stats.ctor_fills = (stats.ctor_fills ?? 0) + 1; + return names.length; + } + + logical(n: e.LogicalExpression): Inst { + let l = this.expr(n.left); + let lbool = this.b.emit("to_boolean", [l], {}); + + let rhs_bb = this.b.newBlock("logical_rhs"); + let join_bb = this.b.newBlock("logical_join"); + let result = join_bb.addParam("logical"); + + if (n.operator === "&&") this.b.condBr(lbool, rhs_bb, [], join_bb, [l]); + else if (n.operator === "||") this.b.condBr(lbool, join_bb, [l], rhs_bb, []); + else throw LowerNotSupported(`logical operator ${n.operator}`, n.loc); + this.b.sealBlock(rhs_bb); + + this.b.setInsertPoint(rhs_bb); + let r = this.expr(n.right); + this.b.br(join_bb, [r]); + this.b.sealBlock(join_bb); + + this.b.setInsertPoint(join_bb); + return result; + } + + unary(n: e.UnaryExpression): Inst { + let arg; + switch (n.operator) { + case "!": + arg = this.expr(n.argument); + return this.b.emit("logical_not", [arg], {}); + case "-": + arg = this.expr(n.argument); + return this.b.emit("neg", [arg], {}); + case "+": + arg = this.expr(n.argument); + return this.b.emit("unary_plus", [arg], {}); + case "~": + arg = this.expr(n.argument); + return this.b.emit("bitnot", [arg], {}); + case "typeof": + arg = this.expr(n.argument); + return this.b.emit("typeof", [arg], {}); + case "void": + // evaluate for side effects, produce undefined (the + // desugar passes' undefinedLit() emits `void 0`) + this.expr(n.argument); + return this.b.constUndefined(); + case "delete": { + // only member expressions (scopes rejected everything else) + const m = n.argument as e.MemberExpression; + const obj = this.expr(m.object as e.Expression); + const key = + !m.computed && m.property.type === "Identifier" + ? this.b.constAtom(m.property.name) + : this.expr(m.property); + return this.b.emit("delete_prop", [obj, key], {}); + } + default: + throw LowerNotSupported(`unary operator ${n.operator}`, n.loc); + } + } + + // the one-time declaration store for a slot-backed toplevel binding: + // unlike writeIdentifier this may store to read-only refs (an exported + // const's initializer is a legitimate store) + writeModuleSlotInit(idNode: e.Identifier, value: Inst): void { + let ref = this.mod_ctx.refs.get(idNode.name); + if (!ref || ref.module === undefined || ref.slot === undefined || ref.slot < 0) + throw LowerNotSupported( + `toplevel declaration of '${idNode.name}' has no slot`, + idNode.loc + ); + this.b.emit("module_slot_store", [value], { module: ref.module, slot: ref.slot }); + } + + // store `value` into this module's export slot named `exportName` + storeExportSlot(exportName: string, value: Inst, loc: e.SourceLocation | null | undefined): void { + let tmi = this.mod_ctx.this_module_info; + let export_info = tmi && tmi.exports.get(exportName); + if (!export_info) + throw LowerNotSupported(`no export slot for '${exportName}'`, loc); + this.b.emit("module_slot_store", [value], { + module: "%self", + slot: export_info.slot_num, + }); + } + + // store `value` into the identifier `idNode` (local binding, writable + // module slot, or global) + writeIdentifier(idNode: e.Identifier, value: Inst): void { + let binding = this.analysis.resolve(idNode); + if (binding === null || binding === undefined) { + let ref = this.mod_ctx.refs.get(idNode.name); + if (ref) { + if (!ref.writable) + throw LowerNotSupported( + `assignment to read-only module binding '${idNode.name}'`, + idNode.loc + ); + this.b.emit("module_slot_store", [value], { + module: ref.module, + slot: ref.slot, + }); + return; + } + this.b.emit("set_global", [value], { atom: idNode.name }); + return; + } + this.writeBinding(binding, value); + } + + assignment(n: e.AssignmentExpression): Inst { + const desugared = compound_assign_ops[n.operator]; + const binop = n.operator === "=" || !desugared ? null : binops[desugared]; + if (n.operator !== "=" && !binop) + throw LowerNotSupported(`assignment operator ${n.operator}`, n.loc); + if (n.left.type === "Identifier") { + let v; + if (binop) { + let cur = this.identifier(n.left); + let rhs = this.expr(n.right); + v = this.b.emit(binop, [cur, rhs], {}); + } else { + v = this.expr(n.right); + } + this.writeIdentifier(n.left, v); + return v; + } + if (n.left.type === "MemberExpression") { + // evaluate the object (and computed key) exactly once + const objNode = n.left.object as e.Expression; + const obj = this.expr(objNode); + let atom: string | null = null; + let key: Inst | null = null; + if (!n.left.computed && n.left.property.type === "Identifier") + atom = n.left.property.name; + else key = this.expr(n.left.property); + let v: Inst; + if (binop) { + const cur = + atom !== null + ? this.propGet(objNode, obj, atom) + : this.b.emit("get_prop", [obj, key!], {}); + const rhs = this.expr(n.right); + v = this.b.emit(binop, [cur, rhs], {}); + } else { + v = this.expr(n.right); + } + if (atom !== null) this.propSet(objNode, obj, atom, v); + else this.b.emit("set_prop", [obj, key!, v], {}); + return v; + } + throw LowerNotSupported(`assignment target ${n.left.type}`, n.loc); + } + + // ++/--: ToNumber(old value) via unary_plus, then add/sub 1 + update(n: e.UpdateExpression): Inst { + let one = this.b.constNumber(1); + let op = n.operator === "++" ? "add" : "sub"; + if (n.argument.type === "Identifier") { + let cur = this.identifier(n.argument); + let old = this.b.emit("unary_plus", [cur], {}); + let nv = this.b.emit(op, [old, one], {}); + this.writeIdentifier(n.argument, nv); + return n.prefix ? nv : old; + } + if (n.argument.type === "MemberExpression") { + const m = n.argument; + const objNode = m.object as e.Expression; + const obj = this.expr(objNode); + let atom: string | null = null; + let key: Inst | null = null; + if (!m.computed && m.property.type === "Identifier") atom = m.property.name; + else key = this.expr(m.property); + const cur = + atom !== null + ? this.propGet(objNode, obj, atom) + : this.b.emit("get_prop", [obj, key!], {}); + const old = this.b.emit("unary_plus", [cur], {}); + const nv = this.b.emit(op, [old, one], {}); + if (atom !== null) this.propSet(objNode, obj, atom, nv); + else this.b.emit("set_prop", [obj, key!, nv], {}); + return n.prefix ? nv : old; + } + throw LowerNotSupported(`update of ${n.argument.type}`, n.loc); + } + + // untagged template literal: the inlined default handler — zip cooked + // strings and ToString'ed substitutions with string_concat (matching + // the legacy handleTemplateDefaultHandlerCall) + template(n: e.TemplateLiteral): Inst { + let strval: Inst | null = null; + const concat = (s: Inst) => { + if (!strval) strval = s; + else strval = this.b.emit("call_runtime", [strval, s], { name: "string_concat" }); + }; + for (let i = 0; i < n.quasis.length; i++) { + const cooked = n.quasis[i]!.value.cooked; + if (cooked.length !== 0) concat(this.b.constAtom(cooked)); + if (i < n.expressions.length) { + const sub = this.expr(n.expressions[i]!); + concat(this.b.emit("call_runtime", [sub], { name: "ToString" })); + } + } + return strval || this.b.constAtom(""); + } + + // tag`lit ${x}` -> tag(callsite, x): the callsite object is a + // per-site cached frozen array (template_callsite); member tags keep + // their receiver as `this`, like any method call + taggedTemplate(n: e.TaggedTemplateExpression): Inst { + let callsite = this.b.emit("template_callsite", [], { + cooked: n.quasi.quasis.map((q) => q.value.cooked), + raw: n.quasi.quasis.map((q) => q.value.raw), + }); + const subs = n.quasi.expressions.map((sub) => this.expr(sub)); + + let callee: Inst, thisArg: Inst; + if (n.tag.type === "MemberExpression") { + thisArg = this.expr(n.tag.object as e.Expression); + if (!n.tag.computed && n.tag.property.type === "Identifier") + callee = this.b.emit("get_prop_atom", [thisArg], { atom: n.tag.property.name }); + else { + let key = this.expr(n.tag.property); + callee = this.b.emit("get_prop", [thisArg, key], {}); + } + } else { + callee = this.expr(n.tag); + thisArg = this.b.constUndefined(); + } + return this.b.emit("call", [callee, thisArg, callsite].concat(subs), {}); + } + + // `ns.member` where ns is a namespace import of a JS module resolves + // to a slot load at compile time (mirroring new-cc's rewrite): the + // module object doesn't answer runtime property lookups for its + // exports. native ("@...") modules DO — they keep the runtime path. + // returns the loaded value, or null if this isn't such an access. + exoticMemberLoad(n: e.MemberExpression): Inst | null { + if (n.object.type !== "Identifier") return null; + let binding = this.analysis.resolve(n.object); + if (binding !== null && binding !== undefined) return null; // shadowed + let ref = this.mod_ctx.refs.get(n.object.name); + if (!ref || ref.exotic === undefined || !ref.module_info) return null; + if (ref.exotic[0] === "@") return null; // native: runtime lookup works + let name = null; + if (!n.computed && n.property.type === "Identifier") name = n.property.name; + else if (n.property.type === "Literal" && typeof n.property.value === "string") + name = n.property.value; + if (name === null) return null; + let export_info = ref.module_info.exports.get(name); + if (!export_info || export_info.promoted) return null; // promoted slots are private + let cv = export_info.constval; + if (cv && cv.type === "Literal" && (cv.value === null || typeof cv.value !== "object")) + return this.literal(cv); + return this.b.emit("module_slot_load", [], { + module: ref.exotic, + slot: export_info.slot_num, + }); + } + + member(n: e.MemberExpression): Inst { + let slotv = this.exoticMemberLoad(n); + if (slotv) return slotv; + let obj = this.expr(n.object); + if (!n.computed && n.property.type === "Identifier") + return this.propGet(n.object as e.Expression, obj, n.property.name); + let key = this.expr(n.property); + return this.b.emit("get_prop", [obj, key], {}); + } + + call(n: e.CallExpression): Inst { + // %-intrinsic calls from the pre-EIR desugar passes lower through + // the table in intrinsics.js (scopes.js already rejected unknowns) + if (n.callee.type === "Identifier" && n.callee.name[0] === "%") + return this.intrinsicCall(n); + let callee, thisArg; + if (n.callee.type === "MemberExpression") { + // ns.member(...) on a JS namespace import: the callee resolves + // to a slot load and `this` is undefined (the legacy rewrite + // turns the member expression into %moduleGetSlot before call + // handling ever sees it) + let slotCallee = this.exoticMemberLoad(n.callee); + if (slotCallee) { + let args = n.arguments.map((a) => this.expr(a)); + return this.b.emit( + "call", + [slotCallee, this.b.constUndefined()].concat(args), + {} + ); + } + thisArg = this.expr(n.callee.object); + if (!n.callee.computed && n.callee.property.type === "Identifier") + callee = this.propGet( + n.callee.object as e.Expression, + thisArg, + n.callee.property.name + ); + else { + let key = this.expr(n.callee.property); + callee = this.b.emit("get_prop", [thisArg, key], {}); + } + } else { + // direct calls: recursion through the self binding skips + // closure dispatch + if (n.callee.type === "Identifier") { + let binding = this.analysis.resolve(n.callee); + if (binding && binding.kind === "self" && binding.fnInfo === this.info) { + let dthis = this.b.constUndefined(); + let dargs = n.arguments.map((a) => this.expr(a)); + return this.b.emit("call", [this.envParam, dthis].concat(dargs), { + direct: this.info.name, + }); + } + } + callee = this.expr(n.callee); + thisArg = this.b.constUndefined(); + } + let args = n.arguments.map((a) => this.expr(a)); + return this.b.emit("call", [callee, thisArg].concat(args), {}); + } + + intrinsicCall(n: e.CallExpression): Inst { + const calleeName = (n.callee as e.Identifier).name; + const intr = eir_intrinsics[calleeName]; + if (!intr) throw LowerNotSupported(`intrinsic ${calleeName}`, n.loc); + let args = n.arguments.map((a) => this.expr(a)); + let v; + if (intr.op) v = this.b.emit(intr.op, args, {}); + else v = this.b.emit("call_runtime", args, { name: intr.runtime, void: intr.void }); + // super() in a derived constructor: the constructed object becomes + // `this` for the rest of the function — including the env copy + // arrows read their lexical this from + if (intr.rebindThis) { + this.b.writeVariable("%this", this.b.cur, v); + if (this.info.thisBinding && this.info.thisBinding.captured) + this.writeBinding(this.info.thisBinding, v); + } + return v; + } + + newExpr(n: e.NewExpression): Inst { + let callee = this.expr(n.callee); + let args = n.arguments.map((a) => this.expr(a)); + return this.b.emit("construct", [callee].concat(args), {}); + } + + conditional(n: e.ConditionalExpression): Inst { + let cond = this.expr(n.test); + let cbool = this.b.emit("to_boolean", [cond], {}); + + let then_bb = this.b.newBlock("cond_then"); + let else_bb = this.b.newBlock("cond_else"); + let join_bb = this.b.newBlock("cond_join"); + let result = join_bb.addParam("cond"); + + this.b.condBr(cbool, then_bb, [], else_bb, []); + this.b.sealBlock(then_bb); + this.b.sealBlock(else_bb); + + this.b.setInsertPoint(then_bb); + let tv = this.expr(n.consequent); + this.b.br(join_bb, [tv]); + + this.b.setInsertPoint(else_bb); + let ev = this.expr(n.alternate); + this.b.br(join_bb, [ev]); + this.b.sealBlock(join_bb); + + this.b.setInsertPoint(join_bb); + return result; + } + + // --- statements --------------------------------------------------------------- + + stmt(n: e.Statement): void { + switch (n.type) { + case "BlockStatement": + for (let s of n.body) { + this.stmt(s); + if (this.b.cur.terminated) return; + } + return; + case "VariableDeclaration": + for (let d of n.declarations) { + if (d.id.type === "ObjectPattern") { + this.lowerObjectPatternDecl(d); + continue; + } + if (d.id.type !== "Identifier") + throw LowerNotSupported(`declaration pattern ${d.id.type}`, n.loc); + let binding = this.analysis.resolve(d.id); + if (!binding && this.isToplevel) { + // a slot-backed module declarator (analyzeToplevel + // declared no local): store the initializer through + // the slot. const-literal folds have no storage — + // their (literal) initializer is dropped. + let ref = this.mod_ctx.refs.get(d.id.name); + if (ref && ref.module === null) continue; + let v = d.init ? this.expr(d.init) : this.b.constUndefined(); + this.writeModuleSlotInit(d.id, v); + continue; + } + // visible-as-undefined during its own initializer: a + // direct self-reference reads undefined, and a closure + // in the init captures the (env) binding the real value + // is stored into below. free for uncaptured bindings + // (SSA map write only). + this.writeBinding(binding!, this.b.constUndefined()); + const init = d.init ? this.expr(d.init) : this.b.constUndefined(); + this.writeBinding(binding!, init); + } + return; + case "FunctionDeclaration": { + let binding = this.analysis.resolve(n.id); + if (!binding && this.isToplevel) { + // a slot-backed module function: lower it, then store + // its closure to the slot at this statement's source + // position (same hoisting caveat as the legacy + // %moduleSetSlot rewrite) + const childInfo = this.analysis.infoFor(n)!; + lowerOneFunction(childInfo, this.analysis, this.module, this.mod_ctx); + let closure = this.b.emit("make_closure", [this.curEnvValue()], { + fn: childInfo.name, + name: displayNameOf(childInfo), + }); + this.writeModuleSlotInit(n.id, closure); + return; + } + // closure was created (hoisted) at entry; lower the body now + lowerOneFunction(this.analysis.infoFor(n)!, this.analysis, this.module, this.mod_ctx); + return; + } + case "ImportDeclaration": + if (!this.isToplevel) throw LowerNotSupported("import declaration", n.loc); + // module resolution happens in the toplevel scaffolding; + // a bare `import "m"` also touches the module object for + // parity with the legacy %moduleGetExotic rewrite + if (n.specifiers.length === 0) + this.b.emit("module_get_exotic", [], { module: n.source_path!.value }); + return; + case "ExportNamedDeclaration": { + if (!this.isToplevel) throw LowerNotSupported("export declaration", n.loc); + if (n.declaration && !Array.isArray(n.declaration)) return this.stmt(n.declaration); + // export { a as b } from "m": copy the source module's + // slots into ours at init time (matching the legacy + // moduleGetSlot/moduleSetSlot rewrite — a snapshot, not a + // live binding) + if (n.source) { + const source = n.source_path!.value; + let source_info = + this.mod_ctx.module_infos && this.mod_ctx.module_infos.get(source); + if (!source_info || source_info.isNative()) + throw LowerNotSupported(`re-export from '${source}'`, n.loc); + for (let spec of n.specifiers) { + let export_info = source_info.exports.get(spec.local.name); + if (!export_info || export_info.promoted) + throw LowerNotSupported( + `module '${source}' doesn't export '${spec.local.name}'`, + n.loc + ); + let v = this.b.emit("module_slot_load", [], { + module: source, + slot: export_info.slot_num, + }); + this.storeExportSlot(spec.exported.name, v, n.loc); + } + return; + } + // export { A, B as C }: copy the locals' current values + // into the exported slots at this statement's position + for (let spec of n.specifiers) { + let v = this.identifier(spec.local); + this.storeExportSlot(spec.exported.name, v, n.loc); + } + return; + } + case "ExportDefaultDeclaration": { + if (!this.isToplevel) throw LowerNotSupported("export default", n.loc); + const v = this.expr(n.declaration as e.Expression); + this.storeExportSlot("default", v, n.loc); + return; + } + case "ExpressionStatement": + this.expr(n.expression); + return; + case "IfStatement": + return this.ifStmt(n); + case "WhileStatement": + return this.whileStmt(n); + case "DoWhileStatement": + return this.doWhileStmt(n); + case "ForStatement": + return this.forStmt(n); + case "ForOfStatement": + return this.forOfStmt(n); + case "ForInStatement": + return this.forInStmt(n); + case "SwitchStatement": + return this.switchStmt(n); + case "ReturnStatement": { + let rv = n.argument ? this.expr(n.argument) : this.b.constUndefined(); + if (this.finallyCtx.length > 0) { + if (this.runFinalizers(0)) return; // a finalizer overrode control + } + // trusted clone with an f64 result: return the raw f64 + // (unguarded unbox — the same trust as trustedNumeric). + // A return this can't prove leaves a boxed return that the + // structural post-check in specialize.ts rejects, so a + // clone never ships with a sig its returns don't honor. + // (untrusted clones always carry a boxed "any" result.) + if (this.spec && this.spec.trusted && this.spec.result === "f64" && + n.argument && this.operandIsNumber(n.argument)) + rv = this.b.emit("unbox_f64", [rv], {}); + this.b.ret(rv); + return; + } + case "ThrowStatement": + this.b.throwValue(this.expr(n.argument)); + return; + case "TryStatement": + return this.tryStmt(n); + case "LabeledStatement": { + // labels on loops bind to the loop's own blocks (the loop + // lowering claims them); labels on anything else get a + // synthetic exit block for labeled breaks + let body = n.body; + while (body.type === "LabeledStatement") body = body.body; + let isLoop = + body.type === "WhileStatement" || + body.type === "DoWhileStatement" || + body.type === "ForStatement" || + body.type === "ForInStatement" || + body.type === "ForOfStatement"; + if (isLoop) { + this.pendingLabels.push(n.label.name); + this.stmt(n.body); + return; + } + let exit = this.b.newBlock(`label_${n.label.name}`); + this.activeLabels.push({ + name: n.label.name, + breakBlock: exit, + continueBlock: null, + ctxLen: this.finallyCtx.length, + }); + this.stmt(n.body); + this.activeLabels.pop(); + if (!this.b.cur.terminated) this.b.br(exit, []); + this.b.sealBlock(exit); + this.b.setInsertPoint(exit); + return; + } + case "BreakStatement": { + if (n.label) { + let l = this.findLabel(n.label.name, n.loc); + if (this.finallyCtx.length > l.ctxLen) { + if (this.runFinalizers(l.ctxLen)) return; + } + this.b.br(l.breakBlock, []); + return; + } + if (this.breakTargets.length === 0) + throw LowerNotSupported("break outside plain loop/switch", n.loc); + let targetLen = this.breakTargets.length; + let firstCrossed = this.finallyCtx.findIndex((c) => c.breakDepth >= targetLen); + if (firstCrossed !== -1) { + if (this.runFinalizers(firstCrossed)) return; + } + this.b.br(this.breakTargets[targetLen - 1]!, []); + return; + } + case "ContinueStatement": { + if (n.label) { + let l = this.findLabel(n.label.name, n.loc); + if (!l.continueBlock) + throw LowerNotSupported(`continue to non-loop label '${n.label.name}'`, n.loc); + if (this.finallyCtx.length > l.ctxLen) { + if (this.runFinalizers(l.ctxLen)) return; + } + this.b.br(l.continueBlock, []); + return; + } + if (this.continueTargets.length === 0) + throw LowerNotSupported("continue outside plain loop", n.loc); + let targetLen = this.continueTargets.length; + let firstCrossed = this.finallyCtx.findIndex((c) => c.continueDepth >= targetLen); + if (firstCrossed !== -1) { + if (this.runFinalizers(firstCrossed)) return; + } + this.b.br(this.continueTargets[targetLen - 1]!, []); + return; + } + case "EmptyStatement": + case "DebuggerStatement": // a no-op in compiled code + return; + default: + throw LowerNotSupported(`statement type ${n.type}`, n.loc); + } + } + + ifStmt(n: e.IfStatement): void { + let cond = this.expr(n.test); + let cbool = this.b.emit("to_boolean", [cond], {}); + + let then_bb = this.b.newBlock("if_then"); + let else_bb = n.alternate ? this.b.newBlock("if_else") : null; + let join_bb = this.b.newBlock("if_join"); + + this.b.condBr(cbool, then_bb, [], else_bb || join_bb, []); + this.b.sealBlock(then_bb); + if (else_bb) this.b.sealBlock(else_bb); + + this.b.setInsertPoint(then_bb); + this.stmt(n.consequent); + if (!this.b.cur.terminated) this.b.br(join_bb, []); + + if (else_bb) { + this.b.setInsertPoint(else_bb); + this.stmt(n.alternate!); + if (!this.b.cur.terminated) this.b.br(join_bb, []); + } + this.b.sealBlock(join_bb); + this.b.setInsertPoint(join_bb); + } + + whileStmt(n: e.WhileStatement): void { + let header = this.b.newBlock("while_header"); + let body = this.b.newBlock("while_body"); + let exit = this.b.newBlock("while_exit"); + + this.b.br(header, []); + + this.b.setInsertPoint(header); + let cond = this.expr(n.test); + let cbool = this.b.emit("to_boolean", [cond], {}); + this.b.condBr(cbool, body, [], exit, []); + this.b.sealBlock(body); + + this.breakTargets.push(exit); + this.continueTargets.push(header); + let nlabels = this.claimPendingLabels(exit, header); + this.b.setInsertPoint(body); + let ble = this.enterLoopBody(n); + this.stmt(n.body); + this.leaveLoopBody(ble); + if (!this.b.cur.terminated) this.b.br(header, []); + this.releaseLabels(nlabels); + this.breakTargets.pop(); + this.continueTargets.pop(); + + this.b.sealBlock(header); + this.b.sealBlock(exit); + this.b.setInsertPoint(exit); + } + + doWhileStmt(n: e.DoWhileStatement): void { + let body = this.b.newBlock("do_body"); + let cond_bb = this.b.newBlock("do_cond"); + let exit = this.b.newBlock("do_exit"); + + this.b.br(body, []); + + this.breakTargets.push(exit); + this.continueTargets.push(cond_bb); + let nlabels = this.claimPendingLabels(exit, cond_bb); + this.b.setInsertPoint(body); + let ble = this.enterLoopBody(n); + this.stmt(n.body); + this.leaveLoopBody(ble); + if (!this.b.cur.terminated) this.b.br(cond_bb, []); + this.releaseLabels(nlabels); + this.breakTargets.pop(); + this.continueTargets.pop(); + this.b.sealBlock(cond_bb); + + this.b.setInsertPoint(cond_bb); + let cond = this.expr(n.test); + let cbool = this.b.emit("to_boolean", [cond], {}); + this.b.condBr(cbool, body, [], exit, []); + this.b.sealBlock(body); + this.b.sealBlock(exit); + this.b.setInsertPoint(exit); + } + + forStmt(n: e.ForStatement): void { + // captured let/const loop vars live in a fresh env per iteration: + // the initial env is created before the init declaration runs, and + // each pass through the update block makes a new env, copying the + // loop vars forward (so the update and next test see the copies, + // and closures made in earlier iterations keep their own) + let le = this.analysis.loopEnvOf(n); + let outerEnvVal: Inst | null = null; + if (le) { + outerEnvVal = this.curEnvValue(); + let e = this.b.emit("make_env", [], { size: le.envSize }); + this.b.emit("env_store", [e, outerEnvVal!], { slot: 0 }); + this.b.writeVariable(this.levar(le), this.b.cur, e); + this.activeLoopEnvs.push(le); + } + + if (n.init) { + if (n.init.type === "VariableDeclaration") this.stmt(n.init); + else this.expr(n.init); + } + + let header = this.b.newBlock("for_header"); + let body = this.b.newBlock("for_body"); + let update = this.b.newBlock("for_update"); + let exit = this.b.newBlock("for_exit"); + + this.b.br(header, []); + + this.b.setInsertPoint(header); + if (n.test) { + let cond = this.expr(n.test); + let cbool = this.b.emit("to_boolean", [cond], {}); + this.b.condBr(cbool, body, [], exit, []); + } else { + this.b.br(body, []); + } + this.b.sealBlock(body); + + this.breakTargets.push(exit); + this.continueTargets.push(update); + let nlabels = this.claimPendingLabels(exit, update); + this.b.setInsertPoint(body); + let ble = this.enterLoopBody(n); + this.stmt(n.body); + this.leaveLoopBody(ble); + if (!this.b.cur.terminated) this.b.br(update, []); + this.releaseLabels(nlabels); + this.breakTargets.pop(); + this.continueTargets.pop(); + this.b.sealBlock(update); + + this.b.setInsertPoint(update); + if (le) { + let eold = this.b.readVariable(this.levar(le), this.b.cur); + let enew = this.b.emit("make_env", [], { size: le.envSize }); + this.b.emit("env_store", [enew, outerEnvVal!], { slot: 0 }); + for (let bd of le.bindings) { + let v = this.b.emit("env_load", [eold], { slot: bd.slot }); + this.b.emit("env_store", [enew, v], { slot: bd.slot }); + } + this.b.writeVariable(this.levar(le), this.b.cur, enew); + } + if (n.update) this.expr(n.update); + this.b.br(header, []); + this.b.sealBlock(header); + this.b.sealBlock(exit); + if (le) this.activeLoopEnvs.pop(); + this.b.setInsertPoint(exit); + } + + lowerObjectPatternDecl(d: e.VariableDeclarator): void { + const src = d.init ? this.expr(d.init) : this.b.constUndefined(); + for (const prop of (d.id as e.ObjectPattern).properties) { + const keyName = + prop.key.type === "Identifier" + ? prop.key.name + : String((prop.key as e.Literal).value); + let target = prop.value as e.Pattern; + let dflt: e.Expression | null = null; + if (target.type === "AssignmentPattern") { + dflt = target.right; + target = target.left; + } + const binding = this.analysis.resolve(target)!; + const v = this.propGet(d.init ?? null, src, keyName); + this.writeBinding(binding, v); + if (dflt) { + let isundef = this.b.emit("strict_eq", [v, this.b.constUndefined()], {}); + let ubool = this.b.emit("to_boolean", [isundef], {}); + let dflt_bb = this.b.newBlock(`pat_default_${keyName}`); + let join_bb = this.b.newBlock(`pat_join_${keyName}`); + this.b.condBr(ubool, dflt_bb, [], join_bb, []); + this.b.sealBlock(dflt_bb); + this.b.setInsertPoint(dflt_bb); + const dv = this.expr(dflt); + this.writeBinding(binding, dv); + this.b.br(join_bb, []); + this.b.sealBlock(join_bb); + this.b.setInsertPoint(join_bb); + } + } + } + + // mirrors the legacy DesugarForOf expansion: iterable[Symbol.iterator]() + // once, then `next()` per iteration, testing `.done` and binding `.value` + forOfStmt(n: e.ForOfStatement): void { + // a captured let/const loop var gets a fresh env each iteration + // (created at the top of the body, right before the var is bound); + // no copying between iterations — the binding is (re)assigned from + // the iteration value anyway. an initial env exists before the + // RHS evaluates: scope analysis declares the binding before + // walking the RHS, so a closure there may already capture it + // (reading undefined, matching the legacy alloca behavior). + let le = this.analysis.loopEnvOf(n); + let outerEnvVal: Inst | null = null; + if (le) { + outerEnvVal = this.curEnvValue(); + let e0 = this.b.emit("make_env", [], { size: le.envSize }); + this.b.emit("env_store", [e0, outerEnvVal], { slot: 0 }); + this.b.writeVariable(this.levar(le), this.b.cur, e0); + this.activeLoopEnvs.push(le); + } + + let obj = this.expr(n.right); + let sym = this.b.emit("get_global", [], { atom: "Symbol" }); + let itkey = this.b.emit("get_prop_atom", [sym], { atom: "iterator" }); + let itfn = this.b.emit("get_prop", [obj, itkey], {}); + let iter = this.b.emit("call", [itfn, obj], {}); + + let header = this.b.newBlock("forof_header"); + let body = this.b.newBlock("forof_body"); + let exit = this.b.newBlock("forof_exit"); + + this.b.br(header, []); + + this.b.setInsertPoint(header); + let nextfn = this.b.emit("get_prop_atom", [iter], { atom: "next" }); + let res = this.b.emit("call", [nextfn, iter], {}); + let done = this.b.emit("get_prop_atom", [res], { atom: "done" }); + let dbool = this.b.emit("to_boolean", [done], {}); + this.b.condBr(dbool, exit, [], body, []); + this.b.sealBlock(body); + + this.b.setInsertPoint(body); + if (le) { + let e = this.b.emit("make_env", [], { size: le.envSize }); + this.b.emit("env_store", [e, outerEnvVal!], { slot: 0 }); + this.b.writeVariable(this.levar(le), this.b.cur, e); + } + const v = this.b.emit("get_prop_atom", [res], { atom: "value" }); + if (n.left.type === "VariableDeclaration") { + const binding = this.analysis.resolve(n.left.declarations[0]!.id)!; + this.writeBinding(binding, v); + } else { + this.writeIdentifier(n.left as e.Identifier, v); + } + let ble = this.enterLoopBody(n); + this.breakTargets.push(exit); + this.continueTargets.push(header); + let nlabels = this.claimPendingLabels(exit, header); + this.stmt(n.body); + this.leaveLoopBody(ble); + if (!this.b.cur.terminated) this.b.br(header, []); + this.releaseLabels(nlabels); + this.breakTargets.pop(); + this.continueTargets.pop(); + + this.b.sealBlock(header); + this.b.sealBlock(exit); + if (le) this.activeLoopEnvs.pop(); + this.b.setInsertPoint(exit); + } + + // mirrors the legacy visitForIn: prop_iterator_new once, then + // prop_iterator_next / prop_iterator_current per iteration. the + // iterator value is opaque (not an ejsval) and must stay a direct + // instruction reference — never a block argument. + forInStmt(n: e.ForInStatement): void { + // fresh env per iteration for a captured let/const binding, with + // an initial env before the RHS evaluates — as in forOfStmt + let le = this.analysis.loopEnvOf(n); + let outerEnvVal: Inst | null = null; + if (le) { + outerEnvVal = this.curEnvValue(); + let e0 = this.b.emit("make_env", [], { size: le.envSize }); + this.b.emit("env_store", [e0, outerEnvVal], { slot: 0 }); + this.b.writeVariable(this.levar(le), this.b.cur, e0); + this.activeLoopEnvs.push(le); + } + + let obj = this.expr(n.right); + let iter = this.b.emit("prop_iter_new", [obj], {}); + + let header = this.b.newBlock("forin_header"); + let body = this.b.newBlock("forin_body"); + let exit = this.b.newBlock("forin_exit"); + + this.b.br(header, []); + + this.b.setInsertPoint(header); + let more = this.b.emit("prop_iter_next", [iter], {}); // i1 + this.b.condBr(more, body, [], exit, []); + this.b.sealBlock(body); + + this.b.setInsertPoint(body); + if (le) { + let e = this.b.emit("make_env", [], { size: le.envSize }); + this.b.emit("env_store", [e, outerEnvVal!], { slot: 0 }); + this.b.writeVariable(this.levar(le), this.b.cur, e); + } + const v = this.b.emit("prop_iter_current", [iter], {}); + if (n.left.type === "VariableDeclaration") { + const binding = this.analysis.resolve(n.left.declarations[0]!.id)!; + this.writeBinding(binding, v); + } else { + this.writeIdentifier(n.left as e.Identifier, v); + } + let ble = this.enterLoopBody(n); + this.breakTargets.push(exit); + this.continueTargets.push(header); + let nlabels = this.claimPendingLabels(exit, header); + this.stmt(n.body); + this.leaveLoopBody(ble); + if (!this.b.cur.terminated) this.b.br(header, []); + this.releaseLabels(nlabels); + this.breakTargets.pop(); + this.continueTargets.pop(); + + this.b.sealBlock(header); + this.b.sealBlock(exit); + if (le) this.activeLoopEnvs.pop(); + this.b.setInsertPoint(exit); + } + + switchStmt(n: e.SwitchStatement): void { + let disc = this.expr(n.discriminant); + let exit = this.b.newBlock("switch_exit"); + let bodies = n.cases.map((c, i) => this.b.newBlock(`case_body${i}`)); + let defaultIdx = n.cases.findIndex((c) => !c.test); + + // test chain, in document order, skipping default + for (let i = 0; i < n.cases.length; i++) { + const test = n.cases[i]!.test; + if (!test) continue; + const tv = this.expr(test); + const cmp = this.b.emit("strict_eq", [disc, tv], {}); + const cbool = this.b.emit("to_boolean", [cmp], {}); + const next_test = this.b.newBlock(`case_test${i}`); + this.b.condBr(cbool, bodies[i]!, [], next_test, []); + this.b.sealBlock(next_test); + this.b.setInsertPoint(next_test); + } + // no test matched: default body, or out + this.b.br(defaultIdx >= 0 ? bodies[defaultIdx]! : exit, []); + + // bodies, in document order, falling through to the next + this.breakTargets.push(exit); + for (let i = 0; i < n.cases.length; i++) { + // all of bodies[i]'s preds exist now: its test edge (above) and + // the fallthrough branch emitted for bodies[i-1] last iteration + this.b.sealBlock(bodies[i]!); + this.b.setInsertPoint(bodies[i]!); + for (const s of n.cases[i]!.consequent) { + this.stmt(s); + if (this.b.cur.terminated) break; + } + if (!this.b.cur.terminated) + this.b.br(i + 1 < n.cases.length ? bodies[i + 1]! : exit, []); + } + this.breakTargets.pop(); + this.b.sealBlock(exit); + this.b.setInsertPoint(exit); + } + + // lower fresh copies of the finalizers from index `from` (outermost of + // the crossed set) inward... actually innermost-first: contexts at + // indexes [from..top] are crossed; run top..from. each copy runs with + // the crossed contexts (and their unwind handlers) removed, so a + // return/break inside a finalizer overrides control per spec, and an + // exception during the copy propagates without re-running it. + // returns true if a finalizer terminated the current block. + runFinalizers(from: number): boolean { + let savedCtx = this.finallyCtx; + let savedHandlers = this.b.handlers; + for (let i = savedCtx.length - 1; i >= from; i--) { + this.finallyCtx = savedCtx.slice(0, i); + this.b.handlers = savedHandlers.slice(0, savedCtx[i]!.handlerDepth); + this.stmt(savedCtx[i]!.node); + if (this.b.cur.terminated) { + this.finallyCtx = savedCtx; + this.b.handlers = savedHandlers; + return true; + } + } + this.finallyCtx = savedCtx; + this.b.handlers = savedHandlers; + return false; + } + + tryStmt(n: e.TryStatement): void { + if (n.finalizer) return this.tryFinallyStmt(n); + let handler = n.handlers[0]; + let catch_bb = this.b.newCatchBlock("catch"); + let join_bb = this.b.newBlock("try_join"); + + this.b.pushHandler(catch_bb); + this.stmt(n.block); + this.b.popHandler(); + if (!this.b.cur.terminated) this.b.br(join_bb, []); + this.b.sealBlock(catch_bb); + + this.b.setInsertPoint(catch_bb); + if (handler!.param) { + const binding = this.analysis.resolve(handler!.param)!; + this.writeBinding(binding, catch_bb.params[0]!); + } + this.stmt(handler!.body); + if (!this.b.cur.terminated) this.b.br(join_bb, []); + this.b.sealBlock(join_bb); + this.b.setInsertPoint(join_bb); + } + + // try/finally via finalizer duplication: one copy on the normal path, + // one in a synthetic catch that rethrows, and copies at each abrupt + // exit site (see runFinalizers). + tryFinallyStmt(n: e.TryStatement): void { + let handler = n.handlers && n.handlers.length > 0 ? n.handlers[0] : null; + let fin_catch = this.b.newCatchBlock("finally_catch"); + let join_bb = this.b.newBlock("finally_join"); + + this.finallyCtx.push({ + node: n.finalizer!, + breakDepth: this.breakTargets.length, + continueDepth: this.continueTargets.length, + handlerDepth: this.b.handlers.length, + }); + this.b.pushHandler(fin_catch); + + if (handler) { + let catch_bb = this.b.newCatchBlock("catch"); + let inner_join = this.b.newBlock("catch_join"); + this.b.pushHandler(catch_bb); + this.stmt(n.block); + this.b.popHandler(); + if (!this.b.cur.terminated) this.b.br(inner_join, []); + this.b.sealBlock(catch_bb); + this.b.setInsertPoint(catch_bb); + if (handler.param) { + const binding = this.analysis.resolve(handler.param)!; + this.writeBinding(binding, catch_bb.params[0]!); + } + this.stmt(handler.body); + if (!this.b.cur.terminated) this.b.br(inner_join, []); + this.b.sealBlock(inner_join); + this.b.setInsertPoint(inner_join); + } else { + this.stmt(n.block); + } + + this.b.popHandler(); + this.finallyCtx.pop(); + + // normal-completion copy + if (!this.b.cur.terminated) { + this.stmt(n.finalizer!); + if (!this.b.cur.terminated) this.b.br(join_bb, []); + } + + // exceptional copy: finalizer, then rethrow + this.b.sealBlock(fin_catch); + this.b.setInsertPoint(fin_catch); + const exc = fin_catch.params[0]!; + this.stmt(n.finalizer!); + if (!this.b.cur.terminated) this.b.throwValue(exc); + + this.b.sealBlock(join_bb); + this.b.setInsertPoint(join_bb); + } + + finish(): Func { + if (!this.b.cur.terminated) this.b.ret(this.b.constUndefined()); + return this.b.finish(); + } +} + +// lower one analyzed function (and, transitively, function declarations / +// expressions inside it) into `module`. +export function lowerAnalyzedFunction(info: FnInfo, analysis: ScopeAnalysis, module: Module, mod_ctx?: ModCtx): Func { + return lowerOneFunction(info, analysis, module, mod_ctx); +} + +function lowerOneFunction(info: FnInfo, analysis: ScopeAnalysis, module: Module, mod_ctx?: ModCtx): Func { + if (info.lowered) return info.fn!; + info.lowered = true; + let lf = new LowerFunction(info, analysis, module, mod_ctx); + if (info.node.body.type === "BlockStatement") { + // a fenced constructor's leading this-store run + // batches into one guarded fill; the remaining statements lower + // exactly as the BlockStatement case would have + const skip = lf.lowerBornShapedCtorPrefix(info.node.body); + for (let i = skip; i < info.node.body.body.length; i++) { + lf.stmt(info.node.body.body[i]!); + if (lf.b.cur.terminated) break; + } + } else lf.b.ret(lf.expr(info.node.body)); // expression-bodied arrow + info.fn = lf.finish(); + module.addFunction(info.fn); + // hoisted closures may reference children whose declaration statement + // was never reached (e.g. behind an early return); every child still + // needs a body in the module. + for (let child of info.children) lowerOneFunction(child, analysis, module, mod_ctx); + return info.fn; +} + +// lower a specialized clone of an already-lowered function. +// Unlike lowerOneFunction this ignores info.lowered/info.fn (the generic +// lowering stands), gives the Func the clone's name and typed sig, and +// lowers oracle-number arithmetic unguarded (SpecMode). Children were +// lowered with the generic pass and are shared by name (make_closure in +// the clone body resolves to the same child Funcs). The caller +// (specialize.ts) owns the structural post-checks and adds the Func to +// the module only when they pass. +export function lowerSpecializedClone( + info: FnInfo, + analysis: ScopeAnalysis, + module: Module, + mod_ctx: ModCtx, + spec: SpecMode +): Func { + const lf = new LowerFunction(info, analysis, module, mod_ctx, spec); + if (info.node.body.type === "BlockStatement") lf.stmt(info.node.body); + else { + // expression-bodied arrow: same typed-return rule as ReturnStatement + let rv = lf.expr(info.node.body); + if (spec.trusted && spec.result === "f64" && lf.operandIsNumber(info.node.body as e.Expression)) + rv = lf.b.emit("unbox_f64", [rv], {}); + lf.b.ret(rv); + } + return lf.finish(); +} + +// lower a FunctionDeclaration/FunctionExpression AST node into a fresh +// module; returns { module, fn } +export function lowerFunctionNode( + n: e.Function, + name?: string, + oracle?: TypeOracle | null +): { module: Module; fn: Func; diamonds: number; shape_guards: number } { + let analysis = new ScopeAnalysis(); + let info = analysis.analyzeFunction(n, name); + let module = new Module(info.name); + let typed_stats: NonNullable = { diamonds: 0 }; + let fn = lowerOneFunction(info, analysis, module, { + refs: new Map(), + oracle: oracle ?? null, + typed_stats, + }); + return { + module: module, + fn: fn, + diamonds: typed_stats.diamonds, + shape_guards: typed_stats.shape_guards ?? 0, + }; +} + +// lower every top-level function declaration in a parsed program +export function lowerProgram(ast: e.Program, moduleName?: string): Module { + let module = new Module(moduleName || "module"); + for (let s of ast.body) { + if (s.type === "FunctionDeclaration") { + let analysis = new ScopeAnalysis(); + let info = analysis.analyzeFunction(s); + lowerOneFunction(info, analysis, module); + } + } + return module; +} diff --git a/lib/eir/lowtier-probe.ts b/lib/eir/lowtier-probe.ts new file mode 100644 index 00000000..df218fd2 --- /dev/null +++ b/lib/eir/lowtier-probe.ts @@ -0,0 +1,148 @@ +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: + */ + +// Hand-built low-tier bodies for the low-tier end-to-end test. Lowering does +// does emit has_tag/unbox_f64/f64_*/box_f64 through the oracle path, but to prove +// the emitted machine code is correct we substitute known bodies into the +// functions of test/eir-lowtier1.js, gated on -flowtier (a debug/test +// hook in the -fno-eir-opt mold). With the flag off nothing here +// runs; the test file behaves identically either way, so it also passes in +// the normal matrix. +// +// The shape built here is exactly the guarded diamond: has_tag both +// operands -> fast block (unbox / f64 op / box) vs slow block (the generic +// op), joining in a BOXED block parameter (raw f64/i1 never crosses a block +// boundary; the verifier enforces that). + +import { FunctionBuilder } from "./builder"; +import { verifyFunction } from "./verifier"; +import type { Func, Module, Inst, Block } from "./ir"; + +// `function (a, b) { return a b; }` as the guarded diamond, +// parameterized over the fast f64 op and its generic slow-path twin. +export function buildArithDiamond(name: string, f64Op: string, genericOp: string): Func { + const fb = new FunctionBuilder(name, ["%env", "%this", "a", "b"]); + const a = fb.readVariable("a", fb.cur); + const b = fb.readVariable("b", fb.cur); + + const chk2 = fb.newBlock("chk2"); + const fast = fb.newBlock("fast"); + const slow = fb.newBlock("slow"); + const join = fb.newBlock("join"); + + const t1 = fb.emit("has_tag", [a], { tag: "number" }); + fb.condBr(t1, chk2, [], slow, []); + fb.sealBlock(chk2); + + fb.setInsertPoint(chk2); + const t2 = fb.emit("has_tag", [b], { tag: "number" }); + fb.condBr(t2, fast, [], slow, []); + fb.sealBlock(fast); + fb.sealBlock(slow); + + fb.setInsertPoint(fast); + const ua = fb.emit("unbox_f64", [a], {}); + const ub = fb.emit("unbox_f64", [b], {}); + const val = fb.emit(f64Op, [ua, ub], {}); + const boxed = fb.emit("box_f64", [val], {}); + fb.writeVariable("res", fast, boxed); + fb.br(join, []); + + fb.setInsertPoint(slow); + const generic = fb.emit(genericOp, [a, b], {}); + fb.writeVariable("res", slow, generic); + fb.br(join, []); + fb.sealBlock(join); + + fb.setInsertPoint(join); + fb.ret(fb.readVariable("res", join)); + + const fn = fb.finish(); + verifyFunction(fn); + return fn; +} + +export function buildLowTierAdd(name: string): Func { + return buildArithDiamond(name, "f64_add", "add"); +} + +// `function (a, b) { return a < b; }`: the fast arm branches on the +// raw i1 from f64_lt and rejoins with boxed booleans. +export function buildLowTierLt(name: string): Func { + const fb = new FunctionBuilder(name, ["%env", "%this", "a", "b"]); + const a = fb.readVariable("a", fb.cur); + const b = fb.readVariable("b", fb.cur); + + const chk2 = fb.newBlock("chk2"); + const fast = fb.newBlock("fast"); + const lt_true = fb.newBlock("lt_true"); + const lt_false = fb.newBlock("lt_false"); + const slow = fb.newBlock("slow"); + const join = fb.newBlock("join"); + + const t1 = fb.emit("has_tag", [a], { tag: "number" }); + fb.condBr(t1, chk2, [], slow, []); + fb.sealBlock(chk2); + + fb.setInsertPoint(chk2); + const t2 = fb.emit("has_tag", [b], { tag: "number" }); + fb.condBr(t2, fast, [], slow, []); + fb.sealBlock(fast); + fb.sealBlock(slow); + + fb.setInsertPoint(fast); + const ua = fb.emit("unbox_f64", [a], {}); + const ub = fb.emit("unbox_f64", [b], {}); + const lt = fb.emit("f64_lt", [ua, ub], {}); + fb.condBr(lt, lt_true, [], lt_false, []); + fb.sealBlock(lt_true); + fb.sealBlock(lt_false); + + fb.setInsertPoint(lt_true); + fb.writeVariable("res", lt_true, fb.constBool(true)); + fb.br(join, []); + + fb.setInsertPoint(lt_false); + fb.writeVariable("res", lt_false, fb.constBool(false)); + fb.br(join, []); + + fb.setInsertPoint(slow); + fb.writeVariable("res", slow, fb.emit("lt", [a, b], {})); + fb.br(join, []); + fb.sealBlock(join); + + fb.setInsertPoint(join); + fb.ret(fb.readVariable("res", join)); + + const fn = fb.finish(); + verifyFunction(fn); + return fn; +} + +const PROBES: Array<{ marker: string; build: (name: string) => Func }> = [ + { marker: "lowtier_add", build: (n) => buildArithDiamond(n, "f64_add", "add") }, + { marker: "lowtier_sub", build: (n) => buildArithDiamond(n, "f64_sub", "sub") }, + { marker: "lowtier_mul", build: (n) => buildArithDiamond(n, "f64_mul", "mul") }, + { marker: "lowtier_div", build: (n) => buildArithDiamond(n, "f64_div", "div") }, + { marker: "lowtier_lt", build: buildLowTierLt }, +]; + +// Swap the probe bodies into a lowered module, in place, preserving each +// function's name (make_closure references functions by name). +export function injectLowTierProbes(module: Module): number { + let injected = 0; + for (let i = 0; i < module.functions.length; i++) { + const fn = module.functions[i]!; + for (const probe of PROBES) { + if (fn.name.indexOf(probe.marker) === -1) continue; + module.functions[i] = probe.build(fn.name); + injected++; + break; + } + } + return injected; +} + +// re-exported for the unit tests' ill-typed-flow constructions +export type { Func, Inst, Block }; diff --git a/lib/eir/ops.ts b/lib/eir/ops.ts new file mode 100644 index 00000000..3cfb0db4 --- /dev/null +++ b/lib/eir/ops.ts @@ -0,0 +1,307 @@ +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: + */ + +// The EIR opcode set and its effect table. See EIRProposal.md. +// +// The effect table is the contract between lowering, the optimizer, and +// the abstract interpreter: every instruction's behavior with respect to +// the heap, exceptions, GC, and calls is declared here, not rediscovered +// by pattern matching. + +export const Effect = { + NONE: 0, + READ: 1 << 0, // reads the JS heap + WRITE: 1 << 1, // writes the JS heap + THROW: 1 << 2, // may throw + GC: 1 << 3, // may allocate / trigger a collection + CALL: 1 << 4, // may reenter arbitrary JS +} as const; + +export interface OpInfo { + // fixed operand count, or -1 for variadic + arity: number; + effects: number; + // names of immediate (non-value) attributes the instruction carries + imms?: readonly string[]; + // ends a block unconditionally + terminator?: boolean; + // terminates its block when it carries explicit normal/unwind targets + may_terminate?: boolean; + // typed signature (the low tier). params: what each operand slot + // accepts — "ejsval" (any boxed value; rejects f64/i1) or "f64". + // result: the Inst.type this op produces. Ops without a sig take and + // produce boxed ejsvals ("any"); the verifier enforces the flow rules. + sig?: { readonly params: readonly ("ejsval" | "f64")[]; readonly result: "any" | "f64" | "i1" }; +} + +const E = Effect; + +// effects shorthand for the generic operators: valueOf/toString hooks mean +// they can call back into JS, which implies read/write/throw/gc. +const GENERIC_OP = E.READ | E.WRITE | E.THROW | E.GC | E.CALL; + +// arity: fixed operand count, or -1 for variadic. +// imms: names of immediate (non-value) attributes the instruction carries. +// terminator: ends a block; targets carry per-edge block-argument lists. +export const OPS = { + // --- constants ------------------------------------------------------- + // imms.kind: "number" | "atom" | "boolean" | "undefined" | "null" + // imms.value: the constant payload (unused for undefined/null) + const: { arity: 0, effects: E.NONE, imms: ["kind", "value"] }, + + // --- generic (high tier) operators ------------------------------------ + add: { arity: 2, effects: GENERIC_OP }, + sub: { arity: 2, effects: GENERIC_OP }, + mul: { arity: 2, effects: GENERIC_OP }, + div: { arity: 2, effects: GENERIC_OP }, + mod: { arity: 2, effects: GENERIC_OP }, + lt: { arity: 2, effects: GENERIC_OP }, + le: { arity: 2, effects: GENERIC_OP }, + gt: { arity: 2, effects: GENERIC_OP }, + ge: { arity: 2, effects: GENERIC_OP }, + loose_eq: { arity: 2, effects: GENERIC_OP }, + loose_neq: { arity: 2, effects: GENERIC_OP }, + bitand: { arity: 2, effects: GENERIC_OP }, + bitor: { arity: 2, effects: GENERIC_OP }, + bitxor: { arity: 2, effects: GENERIC_OP }, + shl: { arity: 2, effects: GENERIC_OP }, + shr: { arity: 2, effects: GENERIC_OP }, + ushr: { arity: 2, effects: GENERIC_OP }, + instanceof: { arity: 2, effects: GENERIC_OP }, + in: { arity: 2, effects: GENERIC_OP }, + neg: { arity: 1, effects: GENERIC_OP }, + unary_plus: { arity: 1, effects: GENERIC_OP }, + bitnot: { arity: 1, effects: GENERIC_OP }, + + // pure predicates / conversions + strict_eq: { arity: 2, effects: E.NONE }, + strict_neq: { arity: 2, effects: E.NONE }, + // to_boolean is pure in ejs (no valueOf involvement) + to_boolean: { arity: 1, effects: E.NONE }, + typeof: { arity: 1, effects: E.GC }, + typeof_is: { arity: 1, effects: E.NONE, imms: ["type"] }, + logical_not: { arity: 1, effects: E.NONE }, + + // --- properties -------------------------------------------------------- + get_prop: { arity: 2, effects: GENERIC_OP }, + set_prop: { arity: 3, effects: GENERIC_OP }, + get_prop_atom: { arity: 1, effects: GENERIC_OP, imms: ["atom"] }, + set_prop_atom: { arity: 2, effects: GENERIC_OP, imms: ["atom"] }, + delete_prop: { arity: 2, effects: GENERIC_OP }, + + // --- globals ------------------------------------------------------------ + get_global: { arity: 0, effects: E.READ | E.THROW | E.GC, imms: ["atom"] }, + set_global: { arity: 1, effects: E.WRITE | E.THROW | E.GC, imms: ["atom"] }, + + // --- closures / environments ------------------------------------------- + // make_env: operand 0 (optional, variadic 0..1) is the parent env + make_env: { arity: -1, effects: E.GC, imms: ["size"] }, + env_load: { arity: 1, effects: E.READ, imms: ["slot"] }, + env_store: { arity: 2, effects: E.WRITE, imms: ["slot"] }, + // imms.fn = the EIR function to target; imms.name = the source-level + // display name (Function.prototype.name) — the internal fn name is + // scope-qualified and must not leak + make_closure: { arity: 1, effects: E.GC, imms: ["fn", "name"] }, + + // --- modules ------------------------------------------------------------- + module_slot_load: { arity: 0, effects: E.READ, imms: ["module", "slot"] }, + module_slot_store: { arity: 1, effects: E.WRITE, imms: ["module", "slot"] }, + module_get_exotic: { arity: 0, effects: E.READ | E.GC, imms: ["module"] }, + + // --- calls ---------------------------------------------------------------- + // call: operands = [callee, this, ...args] + // construct: operands = [callee, ...args] + // either may carry targets [normal, unwind] when inside a protected + // region, in which case it terminates its block. + // call: [callee, this, ...args], or with imms.direct set (a direct + // call to a known EIR function): [env, this, ...args] + call: { arity: -1, effects: GENERIC_OP, may_terminate: true, imms: ["direct"] }, + // imms.direct's typed sibling — a direct call to a + // specialized clone (imms.fn) with an unboxed signature. operands = + // [env, ...args] where each arg slot's type must match the callee + // Func.sig's formal ("f64" formals take raw f64 values); no `this` + // (static callee checks exclude this/arguments/rest/defaults). The + // result type is the callee sig's result, stamped on the Inst by the + // specialization pass and re-checked against the callee by + // verifyModule (per-op sigs can't express callee-dependent typing). + call_typed: { arity: -1, effects: GENERIC_OP, may_terminate: true, imms: ["fn"] }, + construct: { arity: -1, effects: GENERIC_OP, may_terminate: true }, + // super(...) in a derived constructor: [super_ctor, ...args] (or + // [super_ctor, args_array] for the _apply form). calls the super + // constructor with this function's incoming &this and newTarget, + // writes the constructed object through &this, and returns it — + // lowering rebinds `this` to the result. + construct_super: { arity: -1, effects: GENERIC_OP, may_terminate: true }, + construct_super_apply: { arity: 2, effects: GENERIC_OP, may_terminate: true }, + // new Foo(...args): [ctor, args_array]; newTarget = the ctor itself + construct_apply: { arity: 2, effects: GENERIC_OP, may_terminate: true }, + // the calling convention's newTarget argument (undefined unless + // invoked via construct) + new_target: { arity: 0, effects: E.NONE }, + + // --- allocation ------------------------------------------------------------ + // dense: operands are the elements in order (no imms). with holes: + // imms.len = total length, imms.indices[i] = the array index operand i + // lands at — holes stay holes (array_new force-fills, stores skip). + make_array: { arity: -1, effects: E.GC | E.WRITE, imms: ["len", "indices"] }, + // %arrayFromSpread: concatenate the operands (each an array literal + // chunk or an arbitrary iterable) into one fresh array. iterating can + // reenter user JS, hence GENERIC_OP. + array_from_spread: { arity: -1, effects: GENERIC_OP }, + // imms.keys: array of atom names, one per operand + make_object: { arity: -1, effects: E.GC | E.WRITE, imms: ["keys"] }, + // an accessor property on an object literal: [obj, getter, setter] + // (undefined for a missing half); non-computed keys only + define_accessor: { arity: 3, effects: E.GC | E.WRITE, imms: ["atom"] }, + // computed-key accessor: operands [obj, key, accessor]; imms.kind is + // "get" or "set" — each accessor defines separately (partial + // descriptors merge in the runtime) + define_accessor_computed: { arity: 3, effects: E.GC | E.WRITE, imms: ["kind"] }, + // a fresh RegExp per evaluation (ES6 semantics, matching the legacy + // visitLiteral); imms.source/imms.flags are strings + make_regexp: { arity: 0, effects: E.THROW | E.GC, imms: ["source", "flags"] }, + // a tagged template's callsite object: frozen cooked-strings array + // with a frozen .raw, built lazily into a per-site global (emit mints + // the global; the same site always yields the identical object) + template_callsite: { arity: 0, effects: E.GC | E.READ | E.WRITE, imms: ["cooked", "raw"] }, + // the rest-parameter array: arguments from index imms.index onward + // (empty array if argc <= index) + rest_args: { arity: 0, effects: E.GC, imms: ["index"] }, + // the arguments object (built from the raw argc/args) + args_obj: { arity: 0, effects: E.THROW | E.GC }, + // the argument count from imms.index onward, as a boxed number: + // max(argc - index, 0). Minted only by the optimizer's args sinking + // (a rest_args/args_obj whose only uses are `.length` reads folds to + // this and the allocation drains). Reads the immutable + // calling-convention argc — effect NONE — but it IS a frame op: + // never valid in specialized clones or across inlining. + arg_len: { arity: 0, effects: E.NONE, imms: ["index"] }, + + // --- for-in property iteration ------------------------------------------ + // the iterator value is an opaque non-ejsval; it must only be consumed + // directly by the two ops below (never passed as a block argument) + prop_iter_new: { arity: 1, effects: E.READ | E.THROW | E.GC }, + // produces an i1 (like to_boolean): true if a property was advanced to + prop_iter_next: { arity: 1, effects: E.READ | E.WRITE | E.THROW | E.GC }, + prop_iter_current: { arity: 1, effects: E.READ | E.GC }, + + // --- low tier --------------------------------------------------------------- + // imms.tag: the runtime tag tested; only "number" is emitted today + // (mirrors LLVMIRVisitor.isNumber, inheriting its per-target check) + has_tag: { arity: 1, effects: E.NONE, imms: ["tag"], sig: { params: ["ejsval"], result: "i1" } }, + + // --- shapes ---------------------------------------------- + // i1: does the operand's header shape index equal the module-interned + // shape? imms.shape keys Module.shapes (the ordered field list the + // module interns at init, like atoms); the emitter folds the NaN-box + // object check in exactly as isNumber backs has_tag. Effect NONE — a + // pure header compare. + has_shape: { arity: 1, effects: E.NONE, imms: ["shape"], sig: { params: ["ejsval"], result: "i1" } }, + // fixed-slot access on a shape-guarded receiver. imms.shape/imms.slot + // name the guarded shape and the field index within it (the shape imm + // repeats the guard's so the verifier compares instead of infers); + // imms.repr is the FIELD's shape repr ("boxed" | "f64"). Typed + // slots: repr:"f64" produces (slot_load) / consumes (slot_store) a RAW + // f64 under the P2 typed-flow rules — sound because the guard proved + // the field's repr, the shaped-world invariant "shape reprs describe + // slot contents" says an f64 slot holds a number, and the NaN-box + // stores doubles raw, so the 8 bytes at the slot ARE the double. + // slot_load's result type is repr-dependent (f64 for "f64", boxed + // otherwise) — stamped by lowering and re-checked by the verifier, + // the call_typed precedent for typing a per-op table can't express. + // The verifier requires every slot op to be + // dominated by an un-killed has_shape fact on the same value for the + // same shape (see the effect-kill inventory in verifier.ts) — without + // it a stale shape would make the slot addressing itself unsafe (the + // storage word is a MAP pointer in dictionary mode). slot_store + // proves the stored value's repr matches the field: an f64 store takes + // a raw f64 operand (a number by construction — the type system IS the + // proof); a boxed store still requires a dominating has_tag=false fact + // on the stored value, so the store provably never needs a repr + // transition. + slot_load: { arity: 1, effects: E.READ, imms: ["shape", "slot", "repr"] }, + slot_store: { arity: 2, effects: E.WRITE, imms: ["shape", "slot", "repr"] }, + // --- born with their shape ----------------------------- + // a statically-keyed object literal, allocated + installed in one + // runtime call: operands are the initial field values in imms.shape's + // field order. The runtime re-derives the true shape from the actual + // values (a wrong static repr can never mint a lying shape) and falls + // back to today's sequential generic sets whenever the shaped fast + // path doesn't apply — same GC|WRITE effect envelope as make_object. + make_object_shaped: { arity: -1, effects: E.GC | E.WRITE, imms: ["shape"] }, + // a fenced constructor's straight-line this-store prefix, batched onto + // the construct-allocated receiver: operands are [this, values...]. + // Only valid behind a passed has_shape(this, "") — the empty-shape + // guard — which the verifier enforces via the same un-killed-fact + // discipline as slot ops (a non-empty or dictionary-mode receiver + // must take the sequential slow arm, where mid-construction + // observables behave identically). + fill_object_shaped: { arity: -1, effects: E.GC | E.WRITE, imms: ["shape"] }, + // i1: is the runtime's accessor epoch still zero — i.e. has NO user + // code installed anything that could intercept a [[Set]] through a + // fresh object's prototype chain (accessor property, non-writable + // data property, prototype swap; see _ejs_accessor_epoch in + // ejs-object.h)? Minted only by the optimizer's constructor-result + // sinking, guarding a virtualized (allocation-free) construct + // against the interception the deleted stores could have met. One + // global load + compare; READ because the global is mutable. + epoch_check: { arity: 0, effects: E.READ, sig: { params: [], result: "i1" } }, + // a raw f64 constant (imms.value). minted only by the optimizer + // (rawJoinParams' const-number edge roots) and the specialization + // pass; lowering itself always emits boxed `const` numbers. + f64_const: { arity: 0, effects: E.NONE, imms: ["value"], sig: { params: [], result: "f64" } }, + unbox_f64: { arity: 1, effects: E.NONE, sig: { params: ["ejsval"], result: "f64" } }, + box_f64: { arity: 1, effects: E.GC, sig: { params: ["f64"], result: "any" } }, + f64_add: { arity: 2, effects: E.NONE, sig: { params: ["f64", "f64"], result: "f64" } }, + f64_sub: { arity: 2, effects: E.NONE, sig: { params: ["f64", "f64"], result: "f64" } }, + f64_mul: { arity: 2, effects: E.NONE, sig: { params: ["f64", "f64"], result: "f64" } }, + f64_div: { arity: 2, effects: E.NONE, sig: { params: ["f64", "f64"], result: "f64" } }, + f64_lt: { arity: 2, effects: E.NONE, sig: { params: ["f64", "f64"], result: "i1" } }, + call_runtime: { arity: -1, effects: GENERIC_OP, imms: ["name"] }, + + // --- control flow -------------------------------------------------------------- + br: { arity: 0, effects: E.NONE, terminator: true }, + cond_br: { arity: 1, effects: E.NONE, terminator: true }, + return: { arity: 1, effects: E.NONE, terminator: true }, + throw: { arity: 1, effects: E.THROW, terminator: true }, + unreachable: { arity: 0, effects: E.NONE, terminator: true }, + + // block parameter (not written by user code; created by the builder) + blockparam: { arity: 0, effects: E.NONE }, +} as const satisfies Record; + +export type OpName = keyof typeof OPS; + +export function isOpName(op: string): op is OpName { + return Object.prototype.hasOwnProperty.call(OPS, op); +} + +export function opInfo(op: string): OpInfo { + if (!isOpName(op)) throw new Error(`unknown EIR opcode '${op}'`); + return OPS[op]; +} + +// the structural slice of Inst that terminator-ness depends on (ir.ts +// imports from here, so this module can't import Inst without a cycle) +export interface InstLike { + op: string; + targets?: readonly object[] | null; +} + +export function isTerminator(inst: InstLike): boolean { + let info = opInfo(inst.op); + if (info.terminator) return true; + // any may-throw instruction with explicit control-flow targets (a + // normal/unwind pair inside a protected region) terminates its block + if (inst.targets && inst.targets.length > 0) return true; + return false; +} + +export function mayThrow(op: string): boolean { + return (opInfo(op).effects & Effect.THROW) !== 0; +} + +export function isPure(op: string): boolean { + return opInfo(op).effects === Effect.NONE && !opInfo(op).terminator; +} diff --git a/lib/eir/optimize-guards.ts b/lib/eir/optimize-guards.ts new file mode 100644 index 00000000..02625aa9 --- /dev/null +++ b/lib/eir/optimize-guards.ts @@ -0,0 +1,1873 @@ +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: + */ + +// trust-free optimizer passes over the guarded +// arithmetic diamonds (lower.ts numericDiamond). +// +// (a) dominated-guard elimination + guard-region merging: a has_tag +// "number" test on a value already proven number is folded, and +// adjacent diamonds fuse into one guard region with one fast side +// and ONE slow path; +// (b) raw f64 block params for the joins the merge rewires, so the +// merged fast side computes unboxed end-to-end and boxes exactly +// once at the region exit. +// +// Both passes are trust-free: nothing here consumes an oracle claim. +// Every fact is proven from the IR itself, so a wrong oracle upstream +// still only costs speed, never correctness. +// +// ---- Soundness inventory (each rewrite's argument, in one place) ---- +// +// Proven-number facts (provenNumberAt): +// - const kind="number" and box_f64 results are numbers by +// construction. +// - results of the generic ops mul / div / sub are ALWAYS numbers: +// ES semantics (`* / -` apply ToNumber and produce a Number; they +// throw rather than return anything else) and the echojs runtime +// agrees (runtime/ejs-ops.c _ejs_op_{mult,div,sub} only ever return +// NUMBER_TO_EJSVAL(..)). `add` is excluded (string concatenation) +// unless both its operands are proven numbers. +// - a block param is a number if every incoming edge argument is +// proven (each argument's proof holds at the query block too — +// number-ness of an immutable SSA value is position-independent +// once every path establishes it; self-edges are vacuous). +// - dominance facts: if block T is the sole-predecessor TRUE successor +// of `cond_br (has_tag %v "number")` and T dominates B, then every +// path to B passed the guard while it was true; SSA values are +// immutable, so %v is a number at B. This is the "real dominance +// reasoning": the CHK dominator tree (verifier.ts) plus the +// sole-pred-true-edge condition, which is exactly what makes +// entering T equivalent to the guard having held. +// +// Guard folding: a cond_br on a proven has_tag rewrites to br to the +// true target. Removing CFG edges only grows dominance, so folding +// with a momentarily-stale dominator tree is conservative. +// +// Region merging (the diamond CFG's structural argument): a region is +// verified — never assumed — to have the shape +// head: ... cond_br (has_tag) -> fast..., slow +// fast side: blocks whose instructions are all effect-free (at most +// GC), terminated by br / interior number guards (false edges +// all to the region's slow entry) / i1 cond_brs, exiting to the +// join; +// slow side: a linear chain of blocks holding only the whitelisted +// generic ops {add,sub,mul,div,lt} (plus effect-free +// instructions and br), exiting to the same join. +// Merging region R1 with the region R2 headed at R1's join J1: +// - R1's slow exit is retargeted from J1 straight into R2's slow +// entry, and J1's params are substituted with the values that edge +// carried wherever R2's slow chain used them: the slow path becomes +// the full generic computation in original program order (identical +// semantics — the generic ops ARE the JS semantics regardless of +// operand types). A previously slow-then-fast mixed execution now +// runs fully generic: same observable behavior, only slower — the +// documented cost model of guard regions. +// - R2's guard-failure edges are retargeted from R2's slow entry to +// R1's slow entry (the merged region's single slow path). Those +// failures happen only after R1's guards all passed and R1's fast +// side (effect-free by the region check) ran, so the R1 portion of +// the slow chain RE-executes. That is sound because the merge +// first proves every instruction in R1's slow chain is either +// effect-free or a whitelisted generic op whose operands are proven +// numbers at R1's fast exits: a generic op on numbers is pure, non- +// throwing (its unwind edge stays untaken), and returns bit-for-bit +// the f64 result the fast side already computed. +// - J1's predecessors must be EXACTLY R1's exits and J2's exactly +// R2's: a foreign edge into either join would make the substituted +// slow values wrong (J1) or undominated (J2) on the foreign path. +// - BOTH regions' slow chains must be the GENERIC TWIN of their fast +// sides (verifyGenericTwin, applied symmetrically): same arithmetic +// ops in the same order with corresponding operands and +// corresponding join-exit arguments. R2's twin-ness covers the +// R1-slow route that would have taken R2's fast arm; R1's twin-ness +// covers the mirrored route — R2 guard failures after R1's fast arm +// ran, rerouted through R1's slow chain (whose exit values then +// substitute into R2's slow ops). The re-execution purity check +// proves those detours unobservable; twin-ness is what proves their +// VALUES agree with the fast side. Nothing about either arm is +// assumed anymore — both are verified. +// - values defined at J1 (params + the pure instruction prefix ahead +// of R2's guard) that are still used beyond R2 are routed through +// R2's join as new params — fast edges pass the J1 value, the slow +// edge passes its slow-side substitute — after checking that every +// such use IS dominated by that join (else the merge is refused); +// raw-typed (i1/f64) values are never routed — merge refused +// (fail-closed). +// +// Raw f64 joins (pass b): a param is converted only when every incoming +// argument is a box_f64 result (whose only consumers are edges feeding +// converted params), an f64 value, or another converted param. The +// boxes are stripped on the edges, unbox_f64 uses collapse to the param +// itself, and any remaining boxed use re-boxes ONCE at the head of the +// param's block — that is the single box at the region exit. The +// verifier re-checks all of it (see verifier.ts rawJoin rules). + +import { Func, Block, Inst } from "./ir"; +import type { Module, ShapeField, Target } from "./ir"; +import { Effect, opInfo } from "./ops"; +import { + computeRPO, + computeDominators, + dominates, + computeShapeFacts, + shapeFactKey, +} from "./verifier"; +import type { OptStats } from "./optimize"; +import { passes } from "../pass-config"; + +// generic ops that (1) lowering pairs with f64 fast ops, and (2) are +// pure and value-identical to the f64 op when both operands are numbers +// (see the soundness inventory above) +const SLOW_OPS = new Set(["add", "sub", "mul", "div", "lt"]); + +// generic ops whose RESULT is always a number (ES + runtime/ejs-ops.c) +const NUMBER_RESULT_OPS = new Set(["mul", "div", "sub"]); + +function isNumberGuard(inst: Inst): boolean { + return inst.op === "has_tag" && inst.imms["tag"] === "number"; +} + +// --- CFG edge surgery ------------------------------------------------------- + +function removePredEdge(block: Block, inst: Inst, targetIndex: number): void { + block.predEdges = block.predEdges.filter( + (e) => !(e.inst === inst && e.targetIndex === targetIndex) + ); +} + +// point inst.targets[targetIndex] at a new block, maintaining predEdges +function retargetEdge(inst: Inst, targetIndex: number, newBlock: Block, newArgs: Inst[]): void { + const t = inst.targets![targetIndex]!; + removePredEdge(t.block, inst, targetIndex); + t.block = newBlock; + t.args = newArgs; + newBlock.predEdges.push({ inst: inst, targetIndex: targetIndex }); +} + +// replace a block's cond_br terminator with an unconditional br to +// targets[keepIndex] (edge args preserved); the condition goes dead and +// DCE sweeps it later +export function condBrToBr(fn: Func, block: Block, keepIndex: number): void { + const cbr = block.terminator!; + const keep = cbr.targets![keepIndex]!; + removePredEdge(keep.block, cbr, keepIndex); + removePredEdge(cbr.targets![1 - keepIndex]!.block, cbr, 1 - keepIndex); + block.insts.pop(); + cbr.block = null; + const br = new Inst(fn, "br", [], {}); + br.block = block; + block.insts.push(br); + br.addTarget(keep.block, keep.args.slice()); +} + +// drop blocks no longer reachable from entry and rebuild predEdges so +// no stale edges (from deleted blocks) survive +export function sweepUnreachableBlocks(fn: Func): boolean { + const reachable = new Set([fn.entry!]); + const stack: Block[] = [fn.entry!]; + while (stack.length > 0) { + const b = stack.pop()!; + for (const s of b.succs()) { + if (!reachable.has(s)) { + reachable.add(s); + stack.push(s); + } + } + } + if (reachable.size === fn.blocks.length) return false; + fn.blocks = fn.blocks.filter((b) => reachable.has(b)); + for (const b of fn.blocks) b.predEdges = []; + for (const b of fn.blocks) { + const t = b.terminator; + if (!t || !t.targets) continue; + t.targets.forEach((tg, i) => tg.block.predEdges.push({ inst: t, targetIndex: i })); + } + return true; +} + +// --- proven-number reasoning ------------------------------------------------ + +// the value proven number on entry to `b` by b being the sole-pred TRUE +// successor of a number guard (see the soundness inventory) +function blockEntryFact(b: Block): Inst | null { + if (b.predEdges.length !== 1) return null; + const e = b.predEdges[0]!; + if (e.targetIndex !== 0) return null; + if (e.inst.op !== "cond_br") return null; + const cond = e.inst.operands[0]!; + if (!isNumberGuard(cond)) return null; + return cond.operands[0]!; +} + +// a dominance fact: some number guard on v has a sole-pred TRUE +// successor dominating `block`, so every path to `block` proved v +function guardFactAt(v: Inst, block: Block, idom: Map): boolean { + let b: Block = block; + for (;;) { + if (blockEntryFact(b) === v) return true; + const n = idom.get(b); + if (!n || n === b) return false; + b = n; + } +} + +// is v proven number at `block`? Combines value-intrinsic proofs +// (const/box_f64/mul/div/sub, position-independent) with dominance +// facts. Facts are sound inside the recursion too: an SSA value's +// number-ness is immutable, so "every path to `block` passed a guard on +// x" proves x is a number at `block` no matter where x sits in a +// compound proof (an add's operand, a param's incoming argument). +// depth-capped so param cycles terminate. +function provenNumberAt( + v: Inst, + block: Block, + idom: Map, + depth: number = 6 +): boolean { + if (v.op === "const") return v.imms["kind"] === "number"; + if (v.op === "box_f64") return true; + if (NUMBER_RESULT_OPS.has(v.op)) return true; + if (guardFactAt(v, block, idom)) return true; + if (depth <= 0) return false; + if (v.op === "add") + return ( + provenNumberAt(v.operands[0]!, block, idom, depth - 1) && + provenNumberAt(v.operands[1]!, block, idom, depth - 1) + ); + if (v.op === "blockparam" && !v.isException && v.block && !v.block.isCatch) { + const b = v.block; + if (b.predEdges.length === 0) return false; + const argIdx = b.argIndexOfParam(v); + let anyProven = false; + for (const e of b.predEdges) { + const arg = e.inst.targets![e.targetIndex]!.args[argIdx]; + if (!arg) return false; + if (arg === v) continue; // self-edge: vacuous + if (!provenNumberAt(arg, block, idom, depth - 1)) return false; + anyProven = true; + } + return anyProven; + } + return false; +} + +// --- pass (a) part 1: dominated/proven guard folding ------------------------ + +function foldProvenGuards(fn: Func, stats: OptStats): boolean { + let changed = false; + const { rpo } = computeRPO(fn); + const idom = computeDominators(fn, rpo); + // folding only REMOVES edges, so dominance only grows and a + // momentarily-stale idom stays conservative; predEdges (which + // blockEntryFact reads) are maintained live by condBrToBr. + for (const b of fn.blocks) { + const term = b.terminator; + if (!term || term.op !== "cond_br") continue; + const cond = term.operands[0]!; + if (!isNumberGuard(cond)) continue; + if (provenNumberAt(cond.operands[0]!, b, idom)) { + condBrToBr(fn, b, 0); + stats.guards_folded++; + changed = true; + } + } + if (changed) sweepUnreachableBlocks(fn); + return changed; +} + +// --- pass (a) part 2: guard-region recognition + merging -------------------- + +interface EdgeRef { + inst: Inst; + targetIndex: number; +} + +interface GuardRegion { + head: Block; // ends in cond_br on a number guard + fastBlocks: Set; // true-side blocks strictly between head and join + guardFalseEdges: EdgeRef[]; // every guard's false edge (all -> slowEntry) + fastExitEdges: EdgeRef[]; // fast-side edges into the join + slowEntry: Block; + slowChain: Block[]; // slowEntry .. slow exit, linear + slowSet: Set; + slowExitEdge: EdgeRef; // the slow chain's edge into the join + join: Block; +} + +const MAX_REGION_BLOCKS = 40; + +// structurally verify (not assume) the guard-region shape headed at +// `head`. Returns null the moment anything deviates. +function matchRegionAt(head: Block): GuardRegion | null { + const term = head.terminator; + if (!term || term.op !== "cond_br") return null; + const cond = term.operands[0]!; + if (!isNumberGuard(cond)) return null; + const t0 = term.targets![0]!; + const t1 = term.targets![1]!; + if (t0.args.length !== 0 || t1.args.length !== 0) return null; + const slowEntry = t1.block; + if (slowEntry.isCatch || t0.block.isCatch) return null; + if (slowEntry.params.length !== 0) return null; + if (t0.block === slowEntry) return null; + + // --- slow side: a linear chain of whitelisted generic ops + const slowChain: Block[] = []; + const slowSet = new Set(); + let join: Block | null = null; + let slowExitEdge: EdgeRef | null = null; + let sb = slowEntry; + for (;;) { + if (slowChain.length > MAX_REGION_BLOCKS) return null; + if (slowSet.has(sb) || sb === head) return null; + slowChain.push(sb); + slowSet.add(sb); + const bt = sb.terminator; + if (!bt) return null; + for (const inst of sb.insts) { + if (inst === bt) continue; + if (inst.targets && inst.targets.length > 0) return null; + if (!SLOW_OPS.has(inst.op) && opInfo(inst.op).effects !== Effect.NONE) return null; + } + let exit: EdgeRef; + if (bt.op === "br") { + exit = { inst: bt, targetIndex: 0 }; + } else if ( + SLOW_OPS.has(bt.op) && + bt.targets && + bt.targets.length === 2 && + bt.targets[0]!.kind === "normal" + ) { + // a generic op inside a protected region: [normal, unwind] + exit = { inst: bt, targetIndex: 0 }; + } else { + return null; + } + const next = exit.inst.targets![exit.targetIndex]!.block; + if (next.isCatch) return null; + // interior slow blocks are reachable only from the chain; the + // join is the first successor with an outside predecessor + if (next.predEdges.every((e) => slowSet.has(e.inst.block!))) { + sb = next; + continue; + } + join = next; + slowExitEdge = exit; + break; + } + if (!join || join.isCatch || join === head) return null; + + // --- fast side: effect-free blocks from the true target to the join + const fastBlocks = new Set(); + const guardFalseEdges: EdgeRef[] = [{ inst: term, targetIndex: 1 }]; + const fastExitEdges: EdgeRef[] = []; + const work: Block[] = [t0.block]; + while (work.length > 0) { + const fb = work.pop()!; + if (fastBlocks.has(fb)) continue; + if (fastBlocks.size > MAX_REGION_BLOCKS) return null; + if (fb === join || fb === head || slowSet.has(fb) || fb.isCatch) return null; + fastBlocks.add(fb); + const ft: Inst | null = fb.terminator; + if (!ft) return null; + for (const inst of fb.insts) { + if (inst === ft) continue; + if (inst.targets && inst.targets.length > 0) return null; + // at most GC (const/unbox/box/f64_*/has_tag): re-orderable + // around nothing, skippable by nothing — the region never + // skips or repeats fast blocks, this just proves they are + // unobservable when the slow path re-runs their work + if ((opInfo(inst.op).effects & ~Effect.GC) !== 0) return null; + } + if (ft.op === "br") { + const tg: Target = ft.targets![0]!; + // fast-internal br edges may carry args (a previous merge + // leaves former joins — blocks with params — on the fast + // side); the pred check below confirms membership + if (tg.block === join) fastExitEdges.push({ inst: ft, targetIndex: 0 }); + else work.push(tg.block); + } else if (ft.op === "cond_br") { + const c = ft.operands[0]!; + let arms: number[]; + if (isNumberGuard(c)) { + const f = ft.targets![1]!; + if (f.block !== slowEntry || f.args.length !== 0) return null; + guardFalseEdges.push({ inst: ft, targetIndex: 1 }); + arms = [0]; + } else if (c.type === "i1") { + arms = [0, 1]; // f64_lt-style split: both arms stay fast + } else { + return null; + } + for (const i of arms) { + const tg: Target = ft.targets![i]!; + if (tg.block === join) { + fastExitEdges.push({ inst: ft, targetIndex: i }); + } else { + if (tg.args.length !== 0) return null; + work.push(tg.block); + } + } + } else { + return null; // return/throw/invoke inside the fast side + } + } + if (fastExitEdges.length === 0) return null; + // the fast side is entered only through the head's guard + for (const fb of fastBlocks) { + for (const e of fb.predEdges) { + const src = e.inst.block!; + if (src !== head && !fastBlocks.has(src)) return null; + } + } + + return { + head: head, + fastBlocks: fastBlocks, + guardFalseEdges: guardFalseEdges, + fastExitEdges: fastExitEdges, + slowEntry: slowEntry, + slowChain: slowChain, + slowSet: slowSet, + slowExitEdge: slowExitEdge!, + join: join, + }; +} + +// EIR f64 op -> its generic twin +const F64_TO_GENERIC: Record = { + f64_add: "add", + f64_sub: "sub", + f64_mul: "mul", + f64_div: "div", + f64_lt: "lt", +}; + +// Verify that region2's slow chain is the generic rendition of its fast +// side: the same arithmetic ops in the same order, with operands that +// correspond under the box/unbox mapping, and join-exit arguments that +// correspond slot for slot. On number inputs a generic op is pure and +// bit-identical to its f64 twin, so this is exactly the condition under +// which rerouting a would-have-taken-the-fast-arm execution through the +// slow chain preserves behavior. Anything unrecognized refuses. +// +// Correspondence rules (fast value -> the slow value it must equal): +// unbox_f64(x) -> slowOf(x) +// earlier paired f64 op -> that op's slow twin's result +// where slowOf(x): +// box_f64(f) -> f's rule above +// param of an interior fast block -> slowOf(its single incoming arg) +// j1 params / anything else -> x itself (the slow chain sees +// the same SSA value; the merge's sigma rewrites j1 params later) +// +// f64_lt (and hence const-boolean split arms) is refused — the check +// runs on BOTH sides of a merge, so lt regions simply do not merge at +// all; the boolean-twin correspondence would add checking surface for +// shapes with no measured benefit (hypot2/bench stats unaffected). +function verifyGenericTwin(r2: GuardRegion): boolean { + if (r2.fastExitEdges.length !== 1) return false; // lt splits etc. + + const slowOps: Inst[] = []; + for (const sb of r2.slowChain) + for (const inst of sb.insts) if (SLOW_OPS.has(inst.op)) slowOps.push(inst); + + const pair = new Map(); // fast f64 op -> slow twin + + // correspondence is SSA identity, with one extension: two const + // instructions with the same kind/value are the same value on every + // path (the merge clones pure prefix consts into the slow chain, so + // an earlier merge's region legitimately references the clone where + // the fast side references the original). Value equality must be + // Object.is, not ===: `0 === -0` would conflate the two zeros (a + // sign flip observable via 1/x — review attack H), while NaN + // consts — which === would needlessly refuse — all denote the one + // JS NaN and correspond. + const corresponds = (want: Inst, actual: Inst | null | undefined): boolean => { + if (!actual) return false; + if (want === actual) return true; + return ( + want.op === "const" && + actual.op === "const" && + want.imms["kind"] === actual.imms["kind"] && + Object.is(want.imms["value"], actual.imms["value"]) + ); + }; + + const slowOfBoxed = (x: Inst, d: number): Inst | null => { + if (d <= 0) return null; + if (x.op === "box_f64") return slowOfF64(x.operands[0]!, d - 1); + if (x.op === "blockparam" && x.block && r2.fastBlocks.has(x.block)) { + const b = x.block; + if (b.predEdges.length !== 1) return null; + const e = b.predEdges[0]!; + const arg = e.inst.targets![e.targetIndex]!.args[b.argIndexOfParam(x)]; + return arg ? slowOfBoxed(arg, d - 1) : null; + } + return x; + }; + const slowOfF64 = (f: Inst, d: number): Inst | null => { + if (d <= 0) return null; + if (f.op === "unbox_f64") return slowOfBoxed(f.operands[0]!, d - 1); + return pair.get(f) ?? null; // must be an already-paired f64 op + }; + + // linear walk of the fast side (single path: guards have one fast + // arm, lt splits are refused above), pairing arithmetic in order + let k = 0; + const seen = new Set(); + let b: Block | null = r2.head.terminator!.targets![0]!.block; + let exitArgs: (Inst | null)[] | null = null; + while (b) { + if (b === r2.join || seen.has(b) || !r2.fastBlocks.has(b)) return false; + seen.add(b); + const t: Inst = b.terminator!; + for (const inst of b.insts) { + if (inst === t) break; + const gop = F64_TO_GENERIC[inst.op]; + if (!gop) continue; // unbox/box/const/has_tag: no twin needed + if (inst.op === "f64_lt") return false; + if (k >= slowOps.length) return false; + const tw = slowOps[k++]!; + if (tw.op !== gop) return false; + for (let i = 0; i < inst.operands.length; i++) { + const want = slowOfF64(inst.operands[i]!, 32); + if (!want || !corresponds(want, tw.operands[i])) return false; + } + pair.set(inst, tw); + } + if (t.op === "br") { + const tg: Target = t.targets![0]!; + if (tg.block === r2.join) { + exitArgs = tg.args; + b = null; + } else b = tg.block; + } else if (t.op === "cond_br" && isNumberGuard(t.operands[0]!)) { + b = t.targets![0]!.block; + } else { + return false; + } + } + if (!exitArgs || k !== slowOps.length) return false; + + // join-exit correspondence: what flows out of the fast arm must be + // what flows out of the slow chain, slot for slot + const slowExitArgs = r2.slowExitEdge.inst.targets![r2.slowExitEdge.targetIndex]!.args; + if (exitArgs.length !== slowExitArgs.length) return false; + for (let i = 0; i < exitArgs.length; i++) { + const fa = exitArgs[i]; + const sa = slowExitArgs[i]; + if (!fa || !sa) return false; + const want = slowOfBoxed(fa, 32); + if (!want || !corresponds(want, sa)) return false; + } + return true; +} + +// merge the region headed at r1.join (if any) into r1. Returns true if +// the CFG changed. All checks precede all mutations. +function tryMergeAt(fn: Func, r1: GuardRegion, idom: Map, stats: OptStats): boolean { + const j1 = r1.join; + const r2 = matchRegionAt(j1); + if (!r2) return false; + const j2 = r2.join; + + // region2 must live strictly below region1 (no sharing, no cycles) + if (j2 === r1.head || j2 === j1 || r1.fastBlocks.has(j2) || r1.slowSet.has(j2)) return false; + if (r2.slowEntry === r1.slowEntry) return false; + for (const b of r2.fastBlocks) + if (r1.fastBlocks.has(b) || r1.slowSet.has(b) || b === r1.head) return false; + for (const b of r2.slowChain) + if (r1.fastBlocks.has(b) || r1.slowSet.has(b) || b === r1.head) return false; + + // j1's predecessors must be exactly region1's exits. A foreign edge + // into j1 means region2's guards are reachable WITHOUT region1 + // having run; the slow-side substitution below would then hand + // region2's slow chain region1's slow values, which hold garbage on + // the foreign path (review attack A). + for (const e of j1.predEdges) { + const src = e.inst.block!; + if (!r1.fastBlocks.has(src) && !r1.slowSet.has(src)) return false; + } + // j2's predecessors must be exactly region2's exits (routing fills + // every edge; a foreign edge would get an undominated value) + for (const e of j2.predEdges) { + const src = e.inst.block!; + if (!r2.fastBlocks.has(src) && !r2.slowSet.has(src)) return false; + } + + // BOTH regions' slow chains must be the GENERIC TWIN of their fast + // sides. Region2: the merge reroutes region1's slow exit straight + // into region2's slow chain — including executions where region2's + // guards would have PASSED pre-merge (e.g. a guard on a mul result, + // which is always a number) and run the fast arm (review attack F). + // Region1, the exact mirror (review attack G): region2's guard + // failures — which happen after region1's FAST side ran — are + // rerouted through region1's slow chain, and region2's slow chain + // is rewritten against region1's SLOW values; the purity + // (re-execution) check below proves that detour unobservable, but + // only twin-ness makes its VALUES identical to what the fast side + // already produced. + if (!verifyGenericTwin(r2)) return false; + if (!verifyGenericTwin(r1)) return false; + + // j1's instruction shape: [effect-free prefix..., guard, cond_br] + const term = j1.terminator!; + const guard = term.operands[0]!; + let prefixEnd = j1.insts.length - 1; + if (guard.block === j1) { + if (j1.insts[j1.insts.length - 2] !== guard) return false; + prefixEnd = j1.insts.length - 2; + // the guard may only feed this cond_br (an extra use would need + // slow-side routing of a raw i1 — not a shape we build) + let extraUse = false; + fn.forEachInst((inst) => { + if (inst === term) return; + for (const o of inst.operands) if (o === guard) extraUse = true; + if (inst.targets) + for (const t of inst.targets) for (const a of t.args) if (a === guard) extraUse = true; + }); + if (extraUse) return false; + } + const prefix: Inst[] = []; + for (let i = 0; i < prefixEnd; i++) { + const q = j1.insts[i]!; + if (q.targets && q.targets.length > 0) return false; + if (opInfo(q.op).effects !== Effect.NONE) return false; + prefix.push(q); + } + + // re-execution check: region2's guard failures jump to r1.slowEntry, + // re-running r1's slow chain after r1's fast side already ran. Every + // instruction there must be effect-free or a whitelisted generic op + // whose operands are proven numbers at ALL of r1's fast exits (the + // only ways into region2's guards). + for (const sb of r1.slowChain) { + for (const inst of sb.insts) { + if (inst.op === "br") continue; + if (SLOW_OPS.has(inst.op)) { + for (const o of inst.operands) { + for (const fe of r1.fastExitEdges) { + if (!provenNumberAt(o, fe.inst.block!, idom)) return false; + } + } + } else if (opInfo(inst.op).effects !== Effect.NONE) { + return false; + } + } + } + + // what the slow path knows each J1-defined value to be + const slowMap = new Map(); + const exitTarget = r1.slowExitEdge.inst.targets![r1.slowExitEdge.targetIndex]!; + for (const p of j1.params) { + const arg = exitTarget.args[j1.argIndexOfParam(p)]; + if (!arg) return false; + slowMap.set(p, arg); + } + + // routing pre-check: every use of a J1-defined value outside region2 + // must be dominated by j2 (it gets a routed param there) + const routed: Inst[] = [...j1.params, ...prefix]; + // per value: uses that need the routed param / the slow substitute + const outsideUses = new Map(); // value -> using insts + for (const v of routed) { + const outs: Inst[] = []; + let ok = true; + fn.forEachInst((inst, blk) => { + if (!ok) return; + let uses = false; + for (const o of inst.operands) if (o === v) uses = true; + if (inst.targets) + for (const t of inst.targets) for (const a of t.args) if (a === v) uses = true; + if (!uses) return; + if (blk === j1 || r2.fastBlocks.has(blk)) return; // stays valid (j1 dominates) + if (r2.slowSet.has(blk)) return; // substituted below + if (!dominates(idom, j2, blk)) { + ok = false; // e.g. a catch handler outside the region + return; + } + outs.push(inst); + }); + if (!ok) return false; + if (outs.length > 0) { + // routed params are ordinary boxed joins; a RAW-typed j1 + // value (an i1/f64 prefix inst) live past j2 would need a + // raw param this pass has no business minting — refuse the + // merge (fail-closed by design: the verifier would reject + // the result anyway, we just decline up front) + if (v.type !== "any") return false; + outsideUses.set(v, outs); + } + } + + // ---- all checks passed; mutate ---- + const mapSlow = (v: Inst): Inst => slowMap.get(v) ?? v; + + // clone the pure prefix into r1's slow exit block so the slow chain + // (and routing) can see those values + const slowExitBlock = r1.slowChain[r1.slowChain.length - 1]!; + const exitInst = r1.slowExitEdge.inst; + for (const q of prefix) { + const clone = new Inst(fn, q.op, q.operands.map(mapSlow), { ...q.imms }); + clone.block = slowExitBlock; + slowExitBlock.insts.splice(slowExitBlock.insts.indexOf(exitInst), 0, clone); + slowMap.set(q, clone); + } + + // r1's slow path now falls through into r2's slow chain: the single + // merged slow path is the full generic computation in program order + retargetEdge(exitInst, r1.slowExitEdge.targetIndex, r2.slowEntry, []); + // r2's guard failures re-enter the merged slow path from the top + for (const ge of r2.guardFalseEdges) retargetEdge(ge.inst, ge.targetIndex, r1.slowEntry, []); + // r2's slow chain computes on the slow-side values + for (const sb of r2.slowChain) { + for (const inst of sb.insts) { + for (let i = 0; i < inst.operands.length; i++) + inst.operands[i] = mapSlow(inst.operands[i]!); + if (inst.targets) + for (const t of inst.targets) + for (let i = 0; i < t.args.length; i++) + if (t.args[i]) t.args[i] = mapSlow(t.args[i]!); + } + } + + // route J1-defined values still used beyond region2 through j2 + for (const entry of outsideUses.entries()) { + const v = entry[0]; + const users = entry[1]; + const vr = j2.addParam(v.nameHint); + vr.type = v.type; + const slot = j2.argIndexOfParam(vr); + for (const e of j2.predEdges) { + const t = e.inst.targets![e.targetIndex]!; + t.args[slot] = r2.slowSet.has(e.inst.block!) ? mapSlow(v) : v; + } + for (const u of users) { + for (let i = 0; i < u.operands.length; i++) if (u.operands[i] === v) u.operands[i] = vr; + if (u.targets) + for (const t of u.targets) + for (let i = 0; i < t.args.length; i++) if (t.args[i] === v) t.args[i] = vr; + } + } + + stats.regions_merged++; + return true; +} + +// --- pass (b): raw f64 params for optimizer-rewired joins ------------------- + +export function rawJoinParams(fn: Func, stats: OptStats): boolean { + // candidates: non-entry, non-catch params whose every incoming arg is + // a box_f64, an f64 value, a number constant (converted to + // a raw f64_const on the edge — a loop accumulator seeded `x = 0` + // now qualifies), itself, or another candidate param + const isNumConst = (v: Inst) => v.op === "const" && v.imms["kind"] === "number"; + const cands = new Set(); + for (const b of fn.blocks) { + if (b.isCatch || b === fn.entry) continue; + if (b.predEdges.length === 0) continue; + for (const p of b.params) { + if (p.isException || p.type !== "any") continue; + let ok = true; + for (const e of b.predEdges) { + const t = e.inst.targets![e.targetIndex]!; + if (t.kind === "unwind") { + ok = false; + break; + } + const arg = t.args[b.argIndexOfParam(p)]; + if (!arg) { + ok = false; + break; + } + if (arg === p || arg.op === "box_f64" || arg.type === "f64") continue; + if (isNumConst(arg)) continue; + if (arg.op === "blockparam" && !arg.isException) continue; // resolved in pruning + ok = false; + break; + } + if (ok) cands.add(p); + } + } + if (cands.size === 0) return false; + + // uses of every box_f64 that feeds a candidate (for the strip check) + const boxUses = new Map(); + fn.forEachInst((inst) => { + const record = (v: Inst, opIndex: number) => { + if (v.op !== "box_f64") return; + const list = boxUses.get(v); + if (list) list.push({ inst: inst, opIndex: opIndex }); + else boxUses.set(v, [{ inst: inst, opIndex: opIndex }]); + }; + for (let i = 0; i < inst.operands.length; i++) record(inst.operands[i]!, i); + if (inst.targets) + for (const t of inst.targets) for (const a of t.args) if (a) record(a, -1); + }); + + // params fed by an edge-arg use of value v (empty if any use is not + // an edge arg) + const paramsFedBy = (v: Inst): Inst[] | null => { + const fed: Inst[] = []; + for (const u of boxUses.get(v) || []) { + if (u.opIndex !== -1) return null; // consumed as an operand + for (const t of u.inst.targets!) { + for (let i = 0; i < t.args.length; i++) { + if (t.args[i] !== v) continue; + const p = t.block.params[i + (t.block.isCatch ? 1 : 0)]; + if (!p) return null; + fed.push(p); + } + } + } + return fed; + }; + + // prune to a fixpoint. Two conditions: + // - every arg is admissible (box_f64 strippable / f64 / candidate); + // - the candidate is ROOTED: some arg chain reaches an actual f64 + // producer. A cycle of params feeding only each other must not + // self-justify — there would be no f64 anywhere in it (the + // verifier would reject the result; refuse it here instead). + let pruned = true; + while (pruned) { + pruned = false; + for (const p of cands) { + const b = p.block!; + const argIdx = b.argIndexOfParam(p); + let keep = true; + for (const e of b.predEdges) { + const arg = e.inst.targets![e.targetIndex]!.args[argIdx]!; + if (arg === p || arg.type === "f64" || isNumConst(arg)) continue; + if (arg.op === "blockparam") { + if (!cands.has(arg)) keep = false; + } else if (arg.op === "box_f64") { + // stripping the box must leave it dead: every use an + // edge arg into a candidate param + const fed = paramsFedBy(arg); + if (!fed || !fed.every((fp) => cands.has(fp) || fp.type === "f64")) + keep = false; + } + if (!keep) break; + } + if (!keep) { + cands.delete(p); + pruned = true; + } + } + // rootedness: propagate from box_f64/f64 args through the + // candidate graph; drop anything unreached + const rooted = new Set(); + let grew = true; + while (grew) { + grew = false; + for (const p of cands) { + if (rooted.has(p)) continue; + const b = p.block!; + const argIdx = b.argIndexOfParam(p); + for (const e of b.predEdges) { + const arg = e.inst.targets![e.targetIndex]!.args[argIdx]!; + // NB: a number const is admissible but NOT a root — a + // const-only join must stay boxed (flag-off code would + // otherwise grow boxes for no typed-region payoff); + // only a real f64/box_f64 producer roots the graph. + if ( + arg.op === "box_f64" || + arg.type === "f64" || + (arg.op === "blockparam" && rooted.has(arg)) + ) { + rooted.add(p); + grew = true; + break; + } + } + } + } + for (const p of cands) { + if (!rooted.has(p)) { + cands.delete(p); + pruned = true; + } + } + } + if (cands.size === 0) return false; + + // convert: retype params, strip boxes on the edges + for (const p of cands) { + p.type = "f64"; + p.rawJoin = true; + stats.raw_join_params++; + const b = p.block!; + const argIdx = b.argIndexOfParam(p); + for (const e of b.predEdges) { + const t = e.inst.targets![e.targetIndex]!; + const arg = t.args[argIdx]!; + if (arg.op === "box_f64") t.args[argIdx] = arg.operands[0]!; + else if (isNumConst(arg)) { + // mint the raw producer on the edge; the boxed const keeps + // its other users and falls to DCE when this was the last + const fc = new Inst(fn, "f64_const", [], { value: arg.imms["value"] }); + const eb = e.inst.block!; + fc.block = eb; + eb.insts.splice(eb.insts.indexOf(e.inst), 0, fc); + t.args[argIdx] = fc; + } + } + } + + // rewrite uses: unbox_f64(p) collapses to p; anything still needing + // a boxed value re-boxes once at the head of p's block (the single + // box at the region exit) + for (const p of cands) { + const b = p.block!; + const unboxes: Inst[] = []; + const boxedUsers: Inst[] = []; + fn.forEachInst((inst) => { + if (inst.op === "unbox_f64" && inst.operands[0] === p) { + if (!inst.targets || inst.targets.length === 0) unboxes.push(inst); + return; + } + let boxedUse = false; + const info = opInfo(inst.op); + inst.operands.forEach((o, i) => { + if (o !== p) return; + const want = info.sig ? info.sig.params[i] : undefined; + if (want !== "f64") boxedUse = true; + }); + if (inst.targets) { + for (const t of inst.targets) { + t.args.forEach((a, i) => { + if (a !== p) return; + const tp = t.block.params[i + (t.block.isCatch ? 1 : 0)]; + if (!tp || tp.type !== "f64") boxedUse = true; + }); + } + } + if (boxedUse) boxedUsers.push(inst); + }); + for (const u of unboxes) { + // u's consumers take f64: p is one now + fn.forEachInst((inst) => { + for (let i = 0; i < inst.operands.length; i++) + if (inst.operands[i] === u) inst.operands[i] = p; + if (inst.targets) + for (const t of inst.targets) + for (let i = 0; i < t.args.length; i++) if (t.args[i] === u) t.args[i] = p; + }); + const ub = u.block!; + ub.insts.splice(ub.insts.indexOf(u), 1); + u.block = null; + } + if (boxedUsers.length > 0) { + const nb = new Inst(fn, "box_f64", [p], {}); + nb.block = b; + b.insts.unshift(nb); + for (const u of boxedUsers) { + const info = opInfo(u.op); + u.operands.forEach((o, i) => { + if (o !== p) return; + const want = info.sig ? info.sig.params[i] : undefined; + if (want !== "f64") u.operands[i] = nb; + }); + if (u.targets) { + for (const t of u.targets) { + t.args.forEach((a, i) => { + if (a !== p) return; + const tp = t.block.params[i + (t.block.isCatch ? 1 : 0)]; + if (!tp || tp.type !== "f64") t.args[i] = nb; + }); + } + } + } + } + } + return true; +} + +// --- boolean-join threading --------------------------------------------------- + +// A comparison that rejoins as boxed booleans and immediately re-tests: +// +// ^t: br -> ^join(const true) ^f: br -> ^join(const false) +// ^join(%p): %b = to_boolean %p; cond_br %b -> ^then, ^else +// +// threads each constant edge straight to the cond_br successor it would +// pick (to_boolean(const true/false) is exact), so the fast arm of an +// f64_lt diamond — and a specialized clone's trusted compare — branches on +// the raw i1 with no boxed-boolean round-trip (and no _ejs_truthy call) +// left in the loop. Trust-free: constants only. Non-constant edges (a +// diamond's generic slow arm) keep the join and the re-test. +export function threadBooleanJoins(fn: Func, stats: OptStats): boolean { + // uses of every value (operands + outgoing edge args), for the + // locality check below + const useCount = new Map(); + const bump = (v: Inst) => useCount.set(v, (useCount.get(v) || 0) + 1); + fn.forEachInst((inst) => { + for (const o of inst.operands) bump(o); + if (inst.targets) for (const t of inst.targets) for (const a of t.args) if (a) bump(a); + }); + + let changed = false; + for (const b of fn.blocks) { + if (b.isCatch || b === fn.entry) continue; + if (b.params.length !== 1 || b.insts.length !== 2) continue; + const p = b.params[0]!; + if (p.isException || p.removed) continue; + const tob = b.insts[0]!; + const br = b.insts[1]!; + if (tob.op !== "to_boolean" || tob.operands[0] !== p) continue; + if (br.op !== "cond_br" || br.operands[0] !== tob) continue; + // the join's OWN definitions must die inside it: a use of the + // param (or the boolean) downstream would lose def-dominates-use + // the moment an edge bypasses the block + if (useCount.get(p) !== 1 || useCount.get(tob) !== 1) continue; + if (!br.targets || br.targets.length !== 2) continue; + const tTrue = br.targets[0]!; + const tFalse = br.targets[1]!; + if (tTrue.block === b || tFalse.block === b) continue; + if (tTrue.args.length !== 0 || tFalse.args.length !== 0) continue; + + // predEdges mutate as edges retarget: snapshot first + for (const e of b.predEdges.slice()) { + const t = e.inst.targets![e.targetIndex]!; + if (t.kind === "unwind") continue; + const arg = t.args[0]; + if (!arg || arg.op !== "const" || arg.imms["kind"] !== "boolean") continue; + const dest = arg.imms["value"] ? tTrue.block : tFalse.block; + retargetEdge(e.inst, e.targetIndex, dest, []); + stats.joins_threaded++; + changed = true; + } + } + if (changed) sweepUnreachableBlocks(fn); + return changed; +} + +// --- shape-guard regions ------------------------------------ +// +// The shape twins of pass (a): consecutive GET diamonds on the same +// receiver and shape merge into one guard region with one slow path, and +// guards proven by an un-killed dominating shape fact fold. All facts +// come from verifier.ts's computeShapeFacts — the same engine the +// verifier re-checks the result with, so a fold or merge this pass gets +// wrong is IR the verifier rejects (trust-free, the raw-join discipline). +// +// ---- Soundness inventory (the shape additions) ---- +// +// - Fact folding: a cond_br on has_shape(v, S) rewrites to br(true) +// when the fact (v, S) holds at the branch. Facts only enter blocks +// on guard edges and die at WRITE|CALL instructions (the effect-kill +// rule — see verifier.ts), so a held fact means the header compare +// provably answers true. Folding removes CFG edges only; a stale +// (pre-fold) fact analysis is conservative, and the fact continues to +// reach the true target THROUGH the folded block (its instructions +// are kill-free on that path, or the fact would not have held). +// - Region shape (matchShapeRegionAt): head ends in cond_br on +// has_shape(recv, S); the fast side is a LINEAR br chain whose +// instructions are effect-free-or-GC plus slot_loads on exactly +// (recv, S); the slow side is the numeric matcher's linear chain with +// get_prop_atom(recv) as the one effectful op. Anything else — a +// store diamond's has_tag split, an interior guard, a foreign edge — +// refuses the match (fail-closed). +// - Merging (tryMergeShapeAt, the numeric merge transplanted): +// region2's guard failures reroute to region1's slow entry, which +// RE-EXECUTES region1's slow chain after region1's fast side already +// ran. That is sound because (a) the fast side and j1 prefix are +// kill-free, so the receiver still has shape S there, and (b) every +// re-executed get_prop_atom names a field OF S — a get of an own +// plain data property: no getter, no proto walk, no effects, and +// bit-identical to the slot_load the fast side already did. The +// TWIN check (verifyShapeTwin) is what proves (b) plus the pairing: +// fast slot_loads and slow gets correspond op for op (atom == the +// shape's field name at that slot, receiver == recv on both sides) +// and join-exit args correspond slot for slot — both regions are +// checked, exactly like the numeric merge's symmetric twin rule. +// - Everything else (j1/j2 pred exactness, pure-prefix cloning, routing +// of j1-defined values through j2 with raw-type refusal) is the +// numeric merge's argument verbatim. +// +// ---- typed slots: the mixed region and the heterogeneous merge ---- +// +// - An f64-repr slot_load produces a raw f64 and lowering boxes it at +// the fast exit, so a shape region's fast side now also carries +// box_f64/unbox_f64 and — after a merge — the f64 arithmetic the +// numeric machinery moved in. The shape matcher therefore admits the +// numeric whitelist in its SLOW chain too (the generic ops are the +// slow rendition of that arithmetic), and the twin check pairs BOTH +// populations: slot_loads with gets (atom == field-at-slot, the twin +// rule) and f64 ops with generic ops (operand correspondence through +// the box/unbox mapping, the numeric rule verbatim). A box_f64 of an +// f64 slot_load corresponds to that load's paired get: the NaN-box +// stores doubles raw, so the get returns bit-for-bit the boxed form +// of the double the load produced. +// - tryMergeShapeNumericAt (the heterogeneous merge): a NUMERIC region +// headed at a shape region's join merges into it — r2's has_tag +// failures reroute to r1's slow entry exactly like a second shape +// region's guard failures would. After the merge r2's head params are +// fed only by r1's fast exits (all box_f64), so foldProvenGuards +// deletes the has_tag and rawJoinParams turns the join raw: the +// region computes unboxed end-to-end, which is the entire point. +// - Re-executing r1's slow chain may now re-run generic arithmetic. +// Sound when each operand is either proven-number at r1's fast exit +// (the numeric merge's rule) or the result of one of r1's own paired +// gets naming an f64-REPR field of the guarded shape: the receiver +// still has shape S (kill-free fast side), an f64-repr slot holds a +// number by the shaped-world invariant, so the get returns a number +// and the generic op is pure and bit-identical to its f64 twin. + +interface ShapeRegion { + head: Block; + recv: Inst; // the guarded receiver value + shapeKey: string; // imms.shape of the head guard + fastBlocks: Set; + fastChain: Block[]; // linear br chain, entry..exit + fastLoads: Inst[]; // slot_loads in chain order + fastArith: Inst[]; // f64 arithmetic in chain order (post-merge) + fastExitEdge: EdgeRef; + slowEntry: Block; + slowChain: Block[]; + slowSet: Set; + slowGets: Inst[]; // get_prop_atom in chain order + slowArith: Inst[]; // whitelisted generic ops in chain order + slowExitEdge: EdgeRef; + join: Block; +} + +// structurally verify the shape-get region headed at `head`; null on any +// deviation. Strictly linear on both sides (see the inventory above). +function matchShapeRegionAt(head: Block): ShapeRegion | null { + const term = head.terminator; + if (!term || term.op !== "cond_br") return null; + const cond = term.operands[0]!; + if (cond.op !== "has_shape") return null; + const recv = cond.operands[0]!; + const shapeKey = String(cond.imms["shape"]); + const t0 = term.targets![0]!; + const t1 = term.targets![1]!; + if (t0.args.length !== 0 || t1.args.length !== 0) return null; + const slowEntry = t1.block; + if (slowEntry.isCatch || t0.block.isCatch) return null; + if (slowEntry.params.length !== 0) return null; + if (t0.block === slowEntry) return null; + + // --- slow side: the numeric matcher's linear chain, with + // get_prop_atom(recv) — and the numeric whitelist ops (the + // generic rendition of merged-in f64 arithmetic) — as the admitted + // effectful ops + const slowChain: Block[] = []; + const slowSet = new Set(); + const slowGets: Inst[] = []; + const slowArith: Inst[] = []; + let join: Block | null = null; + let slowExitEdge: EdgeRef | null = null; + let sb = slowEntry; + for (;;) { + if (slowChain.length > MAX_REGION_BLOCKS) return null; + if (slowSet.has(sb) || sb === head) return null; + slowChain.push(sb); + slowSet.add(sb); + const bt = sb.terminator; + if (!bt) return null; + for (const inst of sb.insts) { + if (inst === bt) continue; + if (inst.targets && inst.targets.length > 0) return null; + if (inst.op === "get_prop_atom") { + if (inst.operands[0] !== recv) return null; + slowGets.push(inst); + } else if (SLOW_OPS.has(inst.op)) { + slowArith.push(inst); + } else if (opInfo(inst.op).effects !== Effect.NONE) { + return null; + } + } + let exit: EdgeRef; + if (bt.op === "br") { + exit = { inst: bt, targetIndex: 0 }; + } else if ( + bt.op === "get_prop_atom" && + bt.targets && + bt.targets.length === 2 && + bt.targets[0]!.kind === "normal" + ) { + // a get inside a protected region: [normal, unwind] + if (bt.operands[0] !== recv) return null; + slowGets.push(bt); + exit = { inst: bt, targetIndex: 0 }; + } else if ( + SLOW_OPS.has(bt.op) && + bt.targets && + bt.targets.length === 2 && + bt.targets[0]!.kind === "normal" + ) { + // a generic op inside a protected region: [normal, unwind] + slowArith.push(bt); + exit = { inst: bt, targetIndex: 0 }; + } else { + return null; + } + const next = exit.inst.targets![exit.targetIndex]!.block; + if (next.isCatch) return null; + if (next.predEdges.every((e) => slowSet.has(e.inst.block!))) { + sb = next; + continue; + } + join = next; + slowExitEdge = exit; + break; + } + if (!join || join.isCatch || join === head) return null; + + // --- fast side: a linear br chain of effect-free-or-GC instructions + // plus slot_loads on exactly (recv, shapeKey); f64 arithmetic (an + // earlier heterogeneous merge's residue) is collected for the twin + const fastBlocks = new Set(); + const fastChain: Block[] = []; + const fastLoads: Inst[] = []; + const fastArith: Inst[] = []; + let fastExitEdge: EdgeRef | null = null; + let fb: Block | null = t0.block; + while (fb) { + if (fastBlocks.has(fb)) return null; + if (fastBlocks.size > MAX_REGION_BLOCKS) return null; + if (fb === join || fb === head || slowSet.has(fb) || fb.isCatch) return null; + fastBlocks.add(fb); + fastChain.push(fb); + const ft = fb.terminator; + if (!ft || ft.op !== "br") return null; // strictly linear + for (const inst of fb.insts) { + if (inst === ft) continue; + if (inst.targets && inst.targets.length > 0) return null; + if (inst.op === "slot_load") { + if (inst.operands[0] !== recv) return null; + if (String(inst.imms["shape"]) !== shapeKey) return null; + fastLoads.push(inst); + } else if (F64_TO_GENERIC[inst.op]) { + fastArith.push(inst); + } else if ((opInfo(inst.op).effects & ~Effect.GC) !== 0) { + return null; + } + } + const tg: Target = ft.targets![0]!; + if (tg.block === join) { + fastExitEdge = { inst: ft, targetIndex: 0 }; + fb = null; + } else { + if (tg.args.length !== 0 && tg.block.params.length === 0) return null; + fb = tg.block; + } + } + if (!fastExitEdge) return null; + // the fast side is entered only through the head's guard + for (const b of fastBlocks) { + for (const e of b.predEdges) { + const src = e.inst.block!; + if (src !== head && !fastBlocks.has(src)) return null; + } + } + + return { + head, + recv, + shapeKey, + fastBlocks, + fastChain, + fastLoads, + fastArith, + fastExitEdge, + slowEntry, + slowChain, + slowSet, + slowGets, + slowArith, + slowExitEdge: slowExitEdge!, + join, + }; +} + +// the slow chain is the generic rendition of the fast side: slot_loads and +// gets pair op for op (atom == the shape's field at that slot), f64 +// arithmetic and generic ops pair op for op with corresponding operands +// (the numeric twin rule), and the join-exit arguments correspond +// slot for slot. A box_f64 of an f64 slot_load corresponds to the load's +// paired get: doubles are stored raw in the NaN-box, so the get returns +// exactly the boxed rendition of the load's raw double. +function verifyShapeTwin(r: ShapeRegion, shapes: Map): boolean { + const fields = shapes.get(r.shapeKey); + if (!fields) return false; + if (r.fastLoads.length !== r.slowGets.length) return false; + if (r.fastArith.length !== r.slowArith.length) return false; + const pair = new Map(); // fast load/arith -> slow twin + for (let i = 0; i < r.fastLoads.length; i++) { + const load = r.fastLoads[i]!; + const get = r.slowGets[i]!; + const slot = load.imms["slot"] as number; + if (typeof slot !== "number" || slot < 0 || slot >= fields.length) return false; + if (fields[slot]!.name !== get.imms["atom"]) return false; + pair.set(load, get); + } + + // const-correspondence, the numeric merge's Object.is rule, extended + // to the raw form a prior rawJoin conversion mints on fast edges + const corresponds = (want: Inst, actual: Inst): boolean => { + if (want === actual) return true; + if ( + want.op === "const" && + actual.op === "const" && + want.imms["kind"] === actual.imms["kind"] && + Object.is(want.imms["value"], actual.imms["value"]) + ) + return true; + return ( + want.op === "f64_const" && + actual.op === "const" && + actual.imms["kind"] === "number" && + Object.is(want.imms["value"], actual.imms["value"]) + ); + }; + + // fast value -> the slow value it must equal at the join. Boxed and + // raw views recurse into each other through box/unbox exactly as the + // numeric twin's slowOfBoxed/slowOfF64 do, with slot_loads bottoming + // out at their paired gets. + const slowOf = (x: Inst, d: number): Inst | null => { + if (d <= 0) return null; + const p = pair.get(x); + if (p) return p; + if (x.op === "box_f64" || x.op === "unbox_f64") return slowOf(x.operands[0]!, d - 1); + if (x.op === "blockparam" && x.block && r.fastBlocks.has(x.block)) { + const b = x.block; + if (b.predEdges.length !== 1) return null; + const e = b.predEdges[0]!; + const arg = e.inst.targets![e.targetIndex]!.args[b.argIndexOfParam(x)]; + return arg ? slowOf(arg, d - 1) : null; + } + return x; // defined above the head: the same SSA value on both sides + }; + + // pair the arithmetic in chain order with corresponding operands. + // f64_lt is refused exactly as the numeric twin refuses it (the check + // runs on both sides of a merge, so lt regions simply do not merge). + for (let i = 0; i < r.fastArith.length; i++) { + const fa = r.fastArith[i]!; + const sa = r.slowArith[i]!; + if (fa.op === "f64_lt") return false; + if (F64_TO_GENERIC[fa.op] !== sa.op) return false; + for (let k = 0; k < fa.operands.length; k++) { + const want = slowOf(fa.operands[k]!, 32); + if (!want || !corresponds(want, sa.operands[k]!)) return false; + } + pair.set(fa, sa); + } + + const fastArgs = r.fastExitEdge.inst.targets![r.fastExitEdge.targetIndex]!.args; + const slowArgs = r.slowExitEdge.inst.targets![r.slowExitEdge.targetIndex]!.args; + if (fastArgs.length !== slowArgs.length) return false; + for (let i = 0; i < fastArgs.length; i++) { + const fa = fastArgs[i]; + const sa = slowArgs[i]; + if (!fa || !sa) return false; + const want = slowOf(fa, 32); + if (!want) return false; + if (!corresponds(want, sa)) return false; + } + return true; +} + +// Re-executing r1's slow chain (a merged region's guard failures reroute +// through it) is sound when every instruction is effect-free, a get of an +// own field of the guarded shape (pure and bit-identical while the +// receiver still has shape S — the fast side is kill-free), or a +// whitelisted generic op each of whose operands is proven-number at r1's +// fast exit or is one of r1's own paired gets naming an f64-REPR field — +// an f64 slot holds a number by the shaped-world invariant, so the +// re-executed generic op is pure and bit-identical to its f64 twin. +function checkShapeSlowReexec( + r1: ShapeRegion, + fields: ShapeField[], + idom: Map +): boolean { + const fastExitBlock = r1.fastExitEdge.inst.block!; + const numberOk = (o: Inst): boolean => { + if (provenNumberAt(o, fastExitBlock, idom)) return true; + if (o.op !== "get_prop_atom" || !r1.slowGets.includes(o)) return false; + const f = fields.find((f) => f.name === o.imms["atom"]); + return f !== undefined && f.repr === "f64"; + }; + for (const sb of r1.slowChain) { + for (const inst of sb.insts) { + if (inst.op === "br") continue; + if (inst.op === "get_prop_atom") { + if (inst.operands[0] !== r1.recv) return false; + if (!fields.some((f) => f.name === inst.imms["atom"])) return false; + } else if (SLOW_OPS.has(inst.op)) { + for (const o of inst.operands) if (!numberOk(o)) return false; + } else if (opInfo(inst.op).effects !== Effect.NONE) { + return false; + } + } + } + return true; +} + +// merge the shape region headed at r1.join (if any) into r1. All checks +// precede all mutations — the numeric tryMergeAt transplanted. +function tryMergeShapeAt( + fn: Func, + shapes: Map, + r1: ShapeRegion, + idom: Map, + stats: OptStats +): boolean { + const j1 = r1.join; + const r2 = matchShapeRegionAt(j1); + if (!r2) return false; + if (r2.recv !== r1.recv || r2.shapeKey !== r1.shapeKey) return false; + const j2 = r2.join; + + // region2 strictly below region1 (no sharing, no cycles) + if (j2 === r1.head || j2 === j1 || r1.fastBlocks.has(j2) || r1.slowSet.has(j2)) return false; + if (r2.slowEntry === r1.slowEntry) return false; + for (const b of r2.fastBlocks) + if (r1.fastBlocks.has(b) || r1.slowSet.has(b) || b === r1.head) return false; + for (const b of r2.slowChain) + if (r1.fastBlocks.has(b) || r1.slowSet.has(b) || b === r1.head) return false; + + // j1's predecessors must be exactly region1's exits, j2's exactly + // region2's (the numeric merge's review attack A) + for (const e of j1.predEdges) { + const src = e.inst.block!; + if (!r1.fastBlocks.has(src) && !r1.slowSet.has(src)) return false; + } + for (const e of j2.predEdges) { + const src = e.inst.block!; + if (!r2.fastBlocks.has(src) && !r2.slowSet.has(src)) return false; + } + + // both regions' slow chains must be their fast sides' generic twins + if (!verifyShapeTwin(r2, shapes)) return false; + if (!verifyShapeTwin(r1, shapes)) return false; + + // j1's instruction shape: [effect-free prefix..., guard, cond_br] + const term = j1.terminator!; + const guard = term.operands[0]!; + let prefixEnd = j1.insts.length - 1; + if (guard.block === j1) { + if (j1.insts[j1.insts.length - 2] !== guard) return false; + prefixEnd = j1.insts.length - 2; + let extraUse = false; + fn.forEachInst((inst) => { + if (inst === term) return; + for (const o of inst.operands) if (o === guard) extraUse = true; + if (inst.targets) + for (const t of inst.targets) for (const a of t.args) if (a === guard) extraUse = true; + }); + if (extraUse) return false; + } else { + return false; // the guard must be j1's own fresh compare + } + const prefix: Inst[] = []; + for (let i = 0; i < prefixEnd; i++) { + const q = j1.insts[i]!; + if (q.targets && q.targets.length > 0) return false; + if (opInfo(q.op).effects !== Effect.NONE) return false; + prefix.push(q); + } + + // re-execution check: region2's guard failures re-run r1's slow chain + // after r1's fast side ran (see checkShapeSlowReexec's argument) + const fields = shapes.get(r1.shapeKey)!; + if (!checkShapeSlowReexec(r1, fields, idom)) return false; + + // what the slow path knows each j1-defined value to be + const slowMap = new Map(); + const exitTarget = r1.slowExitEdge.inst.targets![r1.slowExitEdge.targetIndex]!; + for (const p of j1.params) { + const arg = exitTarget.args[j1.argIndexOfParam(p)]; + if (!arg) return false; + slowMap.set(p, arg); + } + + // routing pre-check (numeric merge verbatim): every use of a + // j1-defined value outside region2 must be dominated by j2 + const routed: Inst[] = [...j1.params, ...prefix]; + const outsideUses = new Map(); + for (const v of routed) { + const outs: Inst[] = []; + let ok = true; + fn.forEachInst((inst, blk) => { + if (!ok) return; + let uses = false; + for (const o of inst.operands) if (o === v) uses = true; + if (inst.targets) + for (const t of inst.targets) for (const a of t.args) if (a === v) uses = true; + if (!uses) return; + if (blk === j1 || r2.fastBlocks.has(blk)) return; + if (r2.slowSet.has(blk)) return; // substituted below + if (!dominates(idom, j2, blk)) { + ok = false; + return; + } + outs.push(inst); + }); + if (!ok) return false; + if (outs.length > 0) { + if (v.type !== "any") return false; // no raw-typed routing + outsideUses.set(v, outs); + } + } + + // ---- all checks passed; mutate ---- + const mapSlow = (v: Inst): Inst => slowMap.get(v) ?? v; + + const slowExitBlock = r1.slowChain[r1.slowChain.length - 1]!; + const exitInst = r1.slowExitEdge.inst; + for (const q of prefix) { + const clone = new Inst(fn, q.op, q.operands.map(mapSlow), { ...q.imms }); + clone.block = slowExitBlock; + slowExitBlock.insts.splice(slowExitBlock.insts.indexOf(exitInst), 0, clone); + slowMap.set(q, clone); + } + + retargetEdge(exitInst, r1.slowExitEdge.targetIndex, r2.slowEntry, []); + retargetEdge(r2.head.terminator!, 1, r1.slowEntry, []); + for (const sb of r2.slowChain) { + for (const inst of sb.insts) { + for (let i = 0; i < inst.operands.length; i++) + inst.operands[i] = mapSlow(inst.operands[i]!); + if (inst.targets) + for (const t of inst.targets) + for (let i = 0; i < t.args.length; i++) + if (t.args[i]) t.args[i] = mapSlow(t.args[i]!); + } + } + + for (const entry of outsideUses.entries()) { + const v = entry[0]; + const users = entry[1]; + const vr = j2.addParam(v.nameHint); + vr.type = v.type; + const slot = j2.argIndexOfParam(vr); + for (const e of j2.predEdges) { + const t = e.inst.targets![e.targetIndex]!; + t.args[slot] = r2.slowSet.has(e.inst.block!) ? mapSlow(v) : v; + } + for (const u of users) { + for (let i = 0; i < u.operands.length; i++) if (u.operands[i] === v) u.operands[i] = vr; + if (u.targets) + for (const t of u.targets) + for (let i = 0; i < t.args.length; i++) if (t.args[i] === v) t.args[i] = vr; + } + } + + stats.shape_regions_merged++; + return true; +} + +// the heterogeneous merge — a NUMERIC guard region headed at a +// shape region's join merges into the shape region, exactly as a second +// shape region would: r2's has_tag failures reroute to r1's slow entry +// (r1's slow chain re-executes — checkShapeSlowReexec — then falls +// through into r2's slow chain, the full generic computation in program +// order). After the merge r2's head params are fed only by r1's fast +// exits, so foldProvenGuards deletes the has_tag and rawJoinParams turns +// the join raw — the region computes unboxed end-to-end. All checks +// precede all mutations; the check set is tryMergeAt's with r1's side +// verified by the mixed shape twin. +function tryMergeShapeNumericAt( + fn: Func, + shapes: Map, + r1: ShapeRegion, + idom: Map, + stats: OptStats +): boolean { + const j1 = r1.join; + const r2 = matchRegionAt(j1); + if (!r2) return false; + const j2 = r2.join; + + // region2 strictly below region1 (no sharing, no cycles) + if (j2 === r1.head || j2 === j1 || r1.fastBlocks.has(j2) || r1.slowSet.has(j2)) return false; + if (r2.slowEntry === r1.slowEntry) return false; + for (const b of r2.fastBlocks) + if (r1.fastBlocks.has(b) || r1.slowSet.has(b) || b === r1.head) return false; + for (const b of r2.slowChain) + if (r1.fastBlocks.has(b) || r1.slowSet.has(b) || b === r1.head) return false; + + // j1's predecessors must be exactly region1's exits, j2's exactly + // region2's (the numeric merge's review attack A) + for (const e of j1.predEdges) { + const src = e.inst.block!; + if (!r1.fastBlocks.has(src) && !r1.slowSet.has(src)) return false; + } + for (const e of j2.predEdges) { + const src = e.inst.block!; + if (!r2.fastBlocks.has(src) && !r2.slowSet.has(src)) return false; + } + + // both slow chains must be their fast sides' generic twins: r2 by the + // numeric rule, r1 by the mixed shape rule + if (!verifyGenericTwin(r2)) return false; + if (!verifyShapeTwin(r1, shapes)) return false; + + // j1's instruction shape: [effect-free prefix..., (guard,) cond_br]. + // The numeric prefix logic verbatim — a has_tag guard is a fact about + // an immutable SSA value, so unlike has_shape it need not be j1's own + // fresh compare. + const term = j1.terminator!; + const guard = term.operands[0]!; + let prefixEnd = j1.insts.length - 1; + if (guard.block === j1) { + if (j1.insts[j1.insts.length - 2] !== guard) return false; + prefixEnd = j1.insts.length - 2; + let extraUse = false; + fn.forEachInst((inst) => { + if (inst === term) return; + for (const o of inst.operands) if (o === guard) extraUse = true; + if (inst.targets) + for (const t of inst.targets) for (const a of t.args) if (a === guard) extraUse = true; + }); + if (extraUse) return false; + } + const prefix: Inst[] = []; + for (let i = 0; i < prefixEnd; i++) { + const q = j1.insts[i]!; + if (q.targets && q.targets.length > 0) return false; + if (opInfo(q.op).effects !== Effect.NONE) return false; + prefix.push(q); + } + + // re-execution check: r2's guard failures re-run r1's slow chain + // after r1's fast side ran (see checkShapeSlowReexec's argument) + const fields = shapes.get(r1.shapeKey); + if (!fields) return false; + if (!checkShapeSlowReexec(r1, fields, idom)) return false; + + // what the slow path knows each j1-defined value to be + const slowMap = new Map(); + const exitTarget = r1.slowExitEdge.inst.targets![r1.slowExitEdge.targetIndex]!; + for (const p of j1.params) { + const arg = exitTarget.args[j1.argIndexOfParam(p)]; + if (!arg) return false; + slowMap.set(p, arg); + } + + // routing pre-check (numeric merge verbatim): every use of a + // j1-defined value outside region2 must be dominated by j2 + const routed: Inst[] = [...j1.params, ...prefix]; + const outsideUses = new Map(); + for (const v of routed) { + const outs: Inst[] = []; + let ok = true; + fn.forEachInst((inst, blk) => { + if (!ok) return; + let uses = false; + for (const o of inst.operands) if (o === v) uses = true; + if (inst.targets) + for (const t of inst.targets) for (const a of t.args) if (a === v) uses = true; + if (!uses) return; + if (blk === j1 || r2.fastBlocks.has(blk)) return; + if (r2.slowSet.has(blk)) return; // substituted below + if (!dominates(idom, j2, blk)) { + ok = false; + return; + } + outs.push(inst); + }); + if (!ok) return false; + if (outs.length > 0) { + if (v.type !== "any") return false; // no raw-typed routing + outsideUses.set(v, outs); + } + } + + // ---- all checks passed; mutate ---- + const mapSlow = (v: Inst): Inst => slowMap.get(v) ?? v; + + const slowExitBlock = r1.slowChain[r1.slowChain.length - 1]!; + const exitInst = r1.slowExitEdge.inst; + for (const q of prefix) { + const clone = new Inst(fn, q.op, q.operands.map(mapSlow), { ...q.imms }); + clone.block = slowExitBlock; + slowExitBlock.insts.splice(slowExitBlock.insts.indexOf(exitInst), 0, clone); + slowMap.set(q, clone); + } + + retargetEdge(exitInst, r1.slowExitEdge.targetIndex, r2.slowEntry, []); + for (const ge of r2.guardFalseEdges) retargetEdge(ge.inst, ge.targetIndex, r1.slowEntry, []); + for (const sb of r2.slowChain) { + for (const inst of sb.insts) { + for (let i = 0; i < inst.operands.length; i++) + inst.operands[i] = mapSlow(inst.operands[i]!); + if (inst.targets) + for (const t of inst.targets) + for (let i = 0; i < t.args.length; i++) + if (t.args[i]) t.args[i] = mapSlow(t.args[i]!); + } + } + + for (const entry of outsideUses.entries()) { + const v = entry[0]; + const users = entry[1]; + const vr = j2.addParam(v.nameHint); + vr.type = v.type; + const slot = j2.argIndexOfParam(vr); + for (const e of j2.predEdges) { + const t = e.inst.targets![e.targetIndex]!; + t.args[slot] = r2.slowSet.has(e.inst.block!) ? mapSlow(v) : v; + } + for (const u of users) { + for (let i = 0; i < u.operands.length; i++) if (u.operands[i] === v) u.operands[i] = vr; + if (u.targets) + for (const t of u.targets) + for (let i = 0; i < t.args.length; i++) if (t.args[i] === v) t.args[i] = vr; + } + } + + stats.shape_numeric_merged++; + return true; +} + +// fold cond_brs on has_shape guards proven by an un-killed dominating +// shape fact (post-merge, region2's guard is exactly this) +function foldProvenShapeGuards(fn: Func, stats: OptStats): boolean { + const analysis = computeShapeFacts(fn); + if (!analysis) return false; + let changed = false; + for (const b of fn.blocks) { + const term = b.terminator; + if (!term || term.op !== "cond_br") continue; + const cond = term.operands[0]!; + if (cond.op !== "has_shape") continue; + // the compare must be b's own: a fact at the BRANCH only proves a + // FRESH compare true. A has_shape computed in an earlier block can + // be stale-false (the receiver transitioned into the shape after + // it ran), and folding a stale-false branch to true would take the + // wrong arm of arbitrary (attack) IR. Same-block suffices: facts + // never appear mid-block, so fact-at-branch implies fact-at-compare. + if (cond.block !== b) continue; + const facts = analysis.factsAt(b, b.insts.length - 1); + if (!facts.has(shapeFactKey(cond.operands[0]!.id, String(cond.imms["shape"])))) continue; + // facts were computed pre-fold; folding only removes edges, so the + // stale analysis is conservative for the remaining candidates + condBrToBr(fn, b, 0); + stats.shape_guards_folded++; + changed = true; + } + if (changed) sweepUnreachableBlocks(fn); + return changed; +} + +// run shape-region merging + fact folding to a fixpoint. Cheap bail when +// the function has no shape guards (every flag-off compile). Merging +// needs the module's shape table for the twin check; without one only +// folding runs (fail-closed). +export function optimizeShapeRegions( + fn: Func, + module: Module | undefined, + stats: OptStats +): boolean { + let hasGuard = false; + for (const b of fn.blocks) { + const t = b.terminator; + if (t && t.op === "cond_br" && t.operands[0]!.op === "has_shape") { + hasGuard = true; + break; + } + } + if (!hasGuard) return false; + + sweepUnreachableBlocks(fn); + + // -fno-shape-fusion disables the + // heterogeneous merge + the in-loop numeric folding, leaving exactly + // the plain shape-region behavior (typed slot ACCESS is a contract + // change and has no off switch — the verifier owns it). + const noFusion = !passes().shapeFusion; + let changedAny = false; + for (let round = 0; round < 50; round++) { + let changed = false; + if (module) { + for (let merges = 0; merges < 50; merges++) { + const { rpo } = computeRPO(fn); + const idom = computeDominators(fn, rpo); + let merged = false; + for (const b of rpo) { + const r1 = matchShapeRegionAt(b); + if (!r1) continue; + if ( + tryMergeShapeAt(fn, module.shapes, r1, idom, stats) || + (!noFusion && tryMergeShapeNumericAt(fn, module.shapes, r1, idom, stats)) + ) { + merged = true; + changed = true; + break; // mutations invalidate matches; re-match + } + } + if (!merged) break; + } + } + if (foldProvenShapeGuards(fn, stats)) changed = true; + // a heterogeneous merge leaves r2's has_tag guards fed only + // by fast-side box_f64 values — provably numbers. Folding them + // here linearizes the fast side so the NEXT round's matcher can + // grow the region further (the fusion cascade). + if (!noFusion && foldProvenGuards(fn, stats)) changed = true; + if (!changed) break; + sweepUnreachableBlocks(fn); + changedAny = true; + } + return changedAny; +} + +// --- driver ----------------------------------------------------------------- + +// run guard folding + region merging to a fixpoint. Cheap bail when the +// function has no number guards (every flag-off compile). +export function optimizeGuardRegions(fn: Func, stats: OptStats): boolean { + let hasGuard = false; + for (const b of fn.blocks) { + const t = b.terminator; + if (t && t.op === "cond_br" && isNumberGuard(t.operands[0]!)) { + hasGuard = true; + break; + } + } + if (!hasGuard) return false; + + // drop builder-era unreachable blocks up front: region matching and + // the routing dominance checks assume every block in fn.blocks is + // reachable (flag-off compiles bailed above and stay byte-pure) + sweepUnreachableBlocks(fn); + + let changedAny = false; + for (let round = 0; round < 50; round++) { + let changed = false; + // merge FIRST, fold after: folding an adjacent diamond's guards + // early dissolves its slow path and leaves a mixed fast/slow + // join in the middle of what should become one region — the + // merged fast side would keep box/unbox round-trips. Merging + // needs no folding to match (interior guard steps and dup + // guards are part of the recognized shape). + for (let merges = 0; merges < 50; merges++) { + const { rpo } = computeRPO(fn); + const idom = computeDominators(fn, rpo); + let merged = false; + for (const b of rpo) { + const r1 = matchRegionAt(b); + if (!r1) continue; + if (tryMergeAt(fn, r1, idom, stats)) { + merged = true; + changed = true; + break; // mutations invalidate matches; re-match + } + } + if (!merged) break; + } + if (foldProvenGuards(fn, stats)) changed = true; + if (!changed) break; + sweepUnreachableBlocks(fn); + changedAny = true; + } + return changedAny; +} diff --git a/lib/eir/optimize.ts b/lib/eir/optimize.ts new file mode 100644 index 00000000..e49d2824 --- /dev/null +++ b/lib/eir/optimize.ts @@ -0,0 +1,1081 @@ +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: + */ + +// EIR optimization passes. The first: allocation sinking (scalar +// replacement) for non-escaping object/array literals, plus the dead +// pure-instruction elimination that sweeps up after it. +// +// The effect table in ops.ts is the contract here: nothing below +// pattern-matches behavior that isn't declared there, with one narrow +// exception — the alloc ops' WRITE effect covers writes to their own +// fresh storage, so a dead allocation is removable even though a dead +// WRITE-effect instruction generally isn't. +// +// Everything is intra-function and flow-insensitive. An allocation is +// sinkable only if every use is a base-position property get/set; a read +// folds only if its key is an own data property that is never written +// (so the initial value flows everywhere without any CFG reasoning — +// non-escape means no one else can write it). Instructions carrying +// explicit normal/unwind targets (may-throw ops inside protected +// regions) are block terminators; we neither fold nor remove them. + +import { Func, Inst, Module, ShapeField, replaceAllUses } from "./ir"; +import { Effect, opInfo } from "./ops"; +import { + condBrToBr, + optimizeGuardRegions, + optimizeShapeRegions, + rawJoinParams, + sweepUnreachableBlocks, + threadBooleanJoins, +} from "./optimize-guards"; +import { sinkFlowAllocations } from "./sink-flow"; +import { cleanupFunction, computeStableSlots, cseModuleSlotLoads } from "./cleanup"; +import { passes } from "../pass-config"; + +export interface OptStats { + allocs_sunk: number; + reads_folded: number; + calls_inlined: number; + iters_folded: number; + dead_removed: number; + // guard-region passes (optimize-guards.ts) + guards_folded: number; + regions_merged: number; + raw_join_params: number; + // shape-guard region passes + shape_guards_folded: number; + shape_regions_merged: number; + // heterogeneous (shape + numeric) region merges + shape_numeric_merged: number; + // unbox_f64(box_f64(x)) round-trips annihilated + unbox_folds: number; + // constant edges threaded past boxed-boolean re-tests + joins_threaded: number; + // non-escaping make_object_shaped scalar-replaced, + // and the shape guards on them resolved statically + shape_allocs_sunk: number; + shape_guards_sunk: number; + // rest_args/args_obj whose only uses were `.length`: reads folded + // to arg_len, allocation removed + args_sunk: number; + // flow-sensitive sinking (sink-flow.ts): written/partially-escaping + // allocations drained, and how many of those materialized a fresh + // object at their single escape site + flow_allocs_sunk: number; + allocs_materialized: number; + // cleanup passes (cleanup.ts, compiler-P1) + consts_folded: number; + branches_folded: number; + params_pruned: number; + typeof_rewrites: number; + lattice_arith: number; + slot_loads_cse: number; +} + +function newStats(): OptStats { + return { + allocs_sunk: 0, + reads_folded: 0, + calls_inlined: 0, + iters_folded: 0, + dead_removed: 0, + guards_folded: 0, + regions_merged: 0, + raw_join_params: 0, + shape_guards_folded: 0, + shape_regions_merged: 0, + shape_numeric_merged: 0, + unbox_folds: 0, + joins_threaded: 0, + shape_allocs_sunk: 0, + shape_guards_sunk: 0, + args_sunk: 0, + flow_allocs_sunk: 0, + allocs_materialized: 0, + consts_folded: 0, + branches_folded: 0, + params_pruned: 0, + typeof_rewrites: 0, + lattice_arith: 0, + slot_loads_cse: 0, + }; +} + +// uses of `value` within fn, with enough position info to classify +export interface Use { + inst: Inst; + // operand index, or -1 for a branch-edge argument + index: number; +} + +// one full-function scan per fixpoint round, shared by every pass in +// the round; the mutation helpers below keep it accurate. storage is a +// plain array indexed by inst.id (dense per-function) — this code runs +// under the echojs runtime during self-compiles, where Map traffic and +// allocation churn are far more expensive than under V8. +const EMPTY_USES: Use[] = []; + +export type UseMap = (Use[] | undefined)[]; + +// per-round scan products: the use map plus every sink pass's +// candidate list, all gathered in ONE forEachInst walk. Under the +// self-hosted runtime each extra walk is real allocation churn +// (for-of iter results per element), and every extra allocation buys +// minor GCs whose conservative pin scans dominate deep-recursion +// compile phases — so the round does exactly one scan, shared. +interface RoundScan { + useMap: UseMap; + objAllocs: Inst[]; // make_object / make_array (sinkAllocations) + shapedAllocs: Inst[]; // make_object_shaped + argsAllocs: Inst[]; // rest_args / args_obj +} + +function scanRound(fn: Func): RoundScan { + const map: UseMap = new Array(fn.next_value_id); + const objAllocs: Inst[] = []; + const shapedAllocs: Inst[] = []; + const argsAllocs: Inst[] = []; + const add = (v: Inst, inst: Inst, index: number) => { + const list = map[v.id]; + if (list) list.push({ inst, index }); + else map[v.id] = [{ inst, index }]; + }; + fn.forEachInst((inst) => { + for (let i = 0; i < inst.operands.length; i++) add(inst.operands[i]!, inst, i); + if (inst.targets) { + for (const t of inst.targets) for (const a of t.args) if (a) add(a, inst, -1); + } + const op = inst.op; + if (op === "make_object" || op === "make_array") objAllocs.push(inst); + else if (op === "make_object_shaped") shapedAllocs.push(inst); + else if (op === "rest_args" || op === "args_obj") argsAllocs.push(inst); + }); + return { useMap: map, objAllocs, shapedAllocs, argsAllocs }; +} + +export function usesOf(uses: UseMap, value: Inst): Use[] { + return uses[value.id] || EMPTY_USES; +} + +function removeInst(uses: UseMap, inst: Inst): void { + const b = inst.block!; + const idx = b.insts.indexOf(inst); + if (idx >= 0) b.insts.splice(idx, 1); + inst.block = null; + // inst no longer uses its operands + for (const o of inst.operands) { + const list = uses[o.id]; + if (list) uses[o.id] = list.filter((u) => u.inst !== inst); + } + uses[inst.id] = undefined; +} + +// --- allocation sinking ---------------------------------------------------- + +// how an allocation's use participates, per classifyUses +interface AllocUses { + // get_prop_atom reads, by atom + atomReads: Inst[]; + // set_prop_atom writes (alloc in base position only) + atomWrites: Inst[]; + // get_prop reads with the alloc as base + computedReads: Inst[]; + escapes: boolean; +} + +// classify every use of a make_object/make_array result. base-position +// gets and atom-keyed sets are the only non-escaping uses; anything else +// (call/return/throw operands, edge arguments, value or key positions, +// computed sets — whose key evaluation we must not disturb — accessor +// defines, deletes) escapes. +function classifyUses(uses: UseMap, alloc: Inst): AllocUses { + const r: AllocUses = { atomReads: [], atomWrites: [], computedReads: [], escapes: false }; + for (const use of usesOf(uses, alloc)) { + const { inst, index } = use; + if (index === -1) { + r.escapes = true; // flows into a block param + } else if (inst.op === "get_prop_atom" && index === 0) { + r.atomReads.push(inst); + } else if (inst.op === "set_prop_atom" && index === 0) { + r.atomWrites.push(inst); + } else if (inst.op === "get_prop" && index === 0) { + r.computedReads.push(inst); + } else { + r.escapes = true; + } + } + return r; +} + +// the own-key initial value for `atom` in a make_object, honoring +// duplicate keys (last definition wins) +function ownObjectValue(alloc: Inst, atom: string): Inst | null { + const keys = alloc.imms.keys as readonly string[]; + for (let i = keys.length - 1; i >= 0; i--) { + if (keys[i] === atom) return alloc.operands[i]!; + } + return null; +} + +// the element initial value for a const-numeric index into a make_array, +// or null for holes / out-of-range / non-element keys +function ownArrayElement(alloc: Inst, index: number): Inst | null { + if (!Number.isInteger(index) || index < 0) return null; + if (alloc.imms.len === undefined) { + // dense: operands are the elements in order + return index < alloc.operands.length ? alloc.operands[index]! : null; + } + // holey: imms.indices[i] is the array index operand i lands at + const indices = alloc.imms.indices as readonly number[]; + for (let i = 0; i < indices.length; i++) { + if (indices[i] === index) return alloc.operands[i]!; + } + return null; +} + +function arrayLength(alloc: Inst): number { + return alloc.imms.len !== undefined ? (alloc.imms.len as number) : alloc.operands.length; +} + +// fold a read to `value`: all the read's uses see the value directly, +// and the read disappears. only for target-less reads — a read with +// unwind targets terminates its block and can't simply vanish. +function foldRead(uses: UseMap, fn: Func, read: Inst, value: Inst): void { + replaceAllUses(fn, read, value); + const inherited = uses[read.id]; + if (inherited && inherited.length > 0) { + const list = uses[value.id]; + if (list) list.push(...inherited); + else uses[value.id] = inherited.slice(); + } + uses[read.id] = undefined; + removeInst(uses, read); +} + +// materialize a `const` number in front of `before` (for .length folds) +function constNumberBefore(fn: Func, before: Inst, value: number): Inst { + const c = new Inst(fn, "const", [], { kind: "number", value: value }); + const b = before.block!; + c.block = b; + b.insts.splice(b.insts.indexOf(before), 0, c); + return c; +} + +// try to scalar-replace one allocation. returns true if anything changed. +function sinkAlloc(useMap: UseMap, fn: Func, alloc: Inst, stats: OptStats): boolean { + const isArray = alloc.op === "make_array"; + const uses = classifyUses(useMap, alloc); + if (uses.escapes) return false; + + let changed = false; + const writtenAtoms = new Set(); + for (const w of uses.atomWrites) writtenAtoms.add(w.imms.atom as string); + + if (isArray) { + // element writes can't reach make_array (set_prop is an escape), + // but a `length` write truncates — it blocks every fold + if (writtenAtoms.size === 0) { + for (const read of uses.atomReads) { + if (read.targets) continue; + if ((read.imms.atom as string) !== "length") continue; // prototype read + foldRead(useMap, fn, read, constNumberBefore(fn, read, arrayLength(alloc))); + stats.reads_folded++; + changed = true; + } + for (const read of uses.computedReads) { + if (read.targets) continue; + const key = read.operands[1]!; + if (key.op !== "const" || key.imms.kind !== "number") continue; + const el = ownArrayElement(alloc, key.imms.value as number); + if (!el) continue; // hole or out of range: prototype read + foldRead(useMap, fn, read, el); + stats.reads_folded++; + changed = true; + } + } + } else { + for (const read of uses.atomReads) { + if (read.targets) continue; + const atom = read.imms.atom as string; + if (writtenAtoms.has(atom)) continue; // flow-sensitive: not yet + const v = ownObjectValue(alloc, atom); + if (!v) continue; // not an own key: prototype read + foldRead(useMap, fn, read, v); + stats.reads_folded++; + changed = true; + } + } + + // if only atom writes remain, the allocation is write-only and dies + // along with its stores — but only stores to OWN keys are provably + // unobservable ([[Set]] to a non-own key walks the prototype chain, + // where a pathological accessor could intercept it). arrays' one + // own atom is `length`. + const ownWrite = (w: Inst) => + isArray + ? (w.imms.atom as string) === "length" + : ownObjectValue(alloc, w.imms.atom as string) !== null; + const remaining = classifyUses(useMap, alloc); + if ( + !remaining.escapes && + remaining.atomReads.length === 0 && + remaining.computedReads.length === 0 && + remaining.atomWrites.every((w) => !w.targets && ownWrite(w)) + ) { + for (const w of remaining.atomWrites) removeInst(useMap, w); + removeInst(useMap, alloc); + stats.allocs_sunk++; + changed = true; + } + return changed; +} + +// --- shaped-literal sinking ------------------------------ +// +// make_object_shaped carries its field values as operands (shape field +// order, boxed) and its shape as an immediate — there are no +// initializing stores. For a non-escaping, never-written shaped +// allocation the birth shape is invariant for the object's whole +// lifetime (nothing else can transition it), so its has_shape guards +// resolve statically and its slot/get reads fold to the operands. +// Guards fold TRUE only when every f64-repr field's operand is provably +// a number (box_f64 or number const) — folding true exposes raw +// slot_loads, and feeding those from a non-number would manufacture +// garbage bits. Folding FALSE is always sound: the diamond's arms are +// twins, and the generic arm's reads fold to the same operands. + +// how a shaped allocation's use participates +interface ShapedAllocUses { + // has_shape guards whose result feeds only their block's cond_br + guards: Inst[]; + // slot_load reads against the birth shape + slotReads: Inst[]; + // get_prop_atom reads (own-field ones fold; others block removal) + atomReads: Inst[]; + // has_shape whose result ALSO flows somewhere else — leaves the + // alloc alive but doesn't escape it + unfoldableGuards: Inst[]; + escapes: boolean; +} + +function classifyShapedUses(useMap: UseMap, alloc: Inst): ShapedAllocUses { + const r: ShapedAllocUses = { + guards: [], + slotReads: [], + atomReads: [], + unfoldableGuards: [], + escapes: false, + }; + for (const use of usesOf(useMap, alloc)) { + const { inst, index } = use; + if (index === -1) { + r.escapes = true; + } else if (inst.op === "has_shape" && index === 0) { + const guardUses = usesOf(useMap, inst); + if ( + guardUses.length === 1 && + guardUses[0]!.inst.op === "cond_br" && + guardUses[0]!.index === 0 && + guardUses[0]!.inst.block === inst.block + ) + r.guards.push(inst); + else r.unfoldableGuards.push(inst); + } else if (inst.op === "slot_load" && index === 0) { + r.slotReads.push(inst); + } else if (inst.op === "get_prop_atom" && index === 0) { + r.atomReads.push(inst); + } else { + // every write (slot_store, set_prop_atom), computed access, + // call/return/throw operand, value position: escape. v1 + // keeps writes out entirely — a write would also invalidate + // the static guard resolution above. + r.escapes = true; + } + } + return r; +} + +// is this operand provably a number (safe to feed a raw f64 slot)? +function provablyNumberOperand(v: Inst): boolean { + return v.op === "box_f64" || (v.op === "const" && v.imms.kind === "number"); +} + +// the raw-f64 replacement for a slot_load of field value `v`, inserted +// before `read` when a fresh const is needed +function rawF64ValueBefore(fn: Func, read: Inst, v: Inst): Inst | null { + if (v.op === "box_f64") return v.operands[0]!; + if (v.op === "const" && v.imms.kind === "number") { + const c = new Inst(fn, "f64_const", [], { value: v.imms.value }); + c.type = "f64"; + const b = read.block!; + c.block = b; + b.insts.splice(b.insts.indexOf(read), 0, c); + return c; + } + return null; +} + +function shapedFieldIndex(fields: readonly ShapeField[], name: string): number { + for (let i = 0; i < fields.length; i++) if (fields[i]!.name === name) return i; + return -1; +} + +// try to scalar-replace one shaped allocation. reads fold immediately; +// guard branches rewrite to their resolved edge (the dead arm and the +// then-unused has_shape are reclaimed by the caller's unreachable-block +// sweep + DCE, and the alloc itself is removed in a later round once +// its use list has drained). +function sinkShapedAlloc( + useMap: UseMap, + fn: Func, + m: Module, + alloc: Inst, + stats: OptStats +): boolean { + const shape = alloc.imms.shape as string; + const fields = m.shapes.get(shape); + if (!fields || fields.length !== alloc.operands.length) return false; + + const uses = classifyShapedUses(useMap, alloc); + if (uses.escapes) return false; + + // guards fold true only when every f64 field's operand is provably + // a number; otherwise the generic arm is the (equally correct) route + const reprsProven = fields.every( + (f, i) => f.repr !== "f64" || provablyNumberOperand(alloc.operands[i]!) + ); + + let changed = false; + + for (const read of uses.slotReads) { + if (read.targets) continue; + if ((read.imms.shape as string) !== shape) continue; // other-shape arm: dies with it + const k = read.imms.slot as number; + if (k < 0 || k >= fields.length) continue; + const v = alloc.operands[k]!; + if ((read.imms.repr as string) === "f64") { + const raw = rawF64ValueBefore(fn, read, v); + if (!raw) continue; // unprovable: the false-folded guard keeps this arm dead + foldRead(useMap, fn, read, raw); + } else { + foldRead(useMap, fn, read, v); + } + stats.reads_folded++; + changed = true; + } + + for (const read of uses.atomReads) { + if (read.targets) continue; + const k = shapedFieldIndex(fields, read.imms.atom as string); + if (k < 0) continue; // prototype read: unfoldable, blocks removal + foldRead(useMap, fn, read, alloc.operands[k]!); + stats.reads_folded++; + changed = true; + } + + for (const guard of uses.guards) { + const block = guard.block!; + const cbr = block.terminator!; + if (cbr.op !== "cond_br") continue; // already rewritten this round + const takeTrue = (guard.imms.shape as string) === shape && reprsProven; + condBrToBr(fn, block, takeTrue ? 0 : 1); + // the cond_br is gone; keep the round's use map accurate + const guardUses = useMap[guard.id]; + if (guardUses) useMap[guard.id] = guardUses.filter((u) => u.inst !== cbr); + stats.shape_guards_sunk++; + changed = true; + } + + // when nothing uses the alloc anymore, it goes now; otherwise the + // next fixpoint round (fresh use map, dead arms swept) finishes + const remaining = usesOf(useMap, alloc).filter((u) => u.inst.block !== null); + if (remaining.length === 0) { + removeInst(useMap, alloc); + stats.shape_allocs_sunk++; + changed = true; + } + return changed; +} + +// the bisect-flag snapshot for one optimizeFunction run, from the +// pass-config registry (compiler-P5; this struct is what generalized +// into it). The old process.env reads lived here — under the +// self-hosted runtime env access is a rebuild-the-whole-environment +// getter, which is why flags are snapshotted per function, never read +// in the fixpoint rounds. +export interface SinkFlags { + noShaped: boolean; + noArgs: boolean; + noFlow: boolean; + noCse: boolean; + noCleanup: boolean; +} + +function readSinkFlags(): SinkFlags { + const cfg = passes(); + return { + noShaped: !cfg.shapedSink, + noArgs: !cfg.argsSink, + noFlow: !cfg.flowSink, + noCse: !cfg.slotCse, + noCleanup: !cfg.eirCleanup, + }; +} + +function sinkAllocations( + useMap: UseMap, + fn: Func, + m: Module | undefined, + stats: OptStats, + noShaped: boolean, + candidates: Inst[], + shapedCandidates: Inst[] +): boolean { + const shaped = noShaped ? [] : shapedCandidates; + let changed = false; + for (const c of candidates) { + if (!c.block) continue; // removed by an earlier candidate's fold + if (sinkAlloc(useMap, fn, c, stats)) changed = true; + } + if (m) { + for (const c of shaped) { + if (!c.block) continue; + if (sinkShapedAlloc(useMap, fn, m, c, stats)) changed = true; + } + } + return changed; +} + +// --- rest_args / args_obj length sinking ----------------------------------- +// +// The arguments object copies argv and synthesizes `.length` from the +// immutable calling-convention argc (before any map or prototype +// consultation); a rest array's length is max(argc - index, 0) at +// birth. So an allocation whose ONLY uses are target-less `.length` +// reads folds to the pure arg_len op and drains — soundly on any +// compile (no shapes involved). Everything else declines: writes +// (even length writes — a rest array's length is writable), computed +// reads (index folds are recorded-declined in docs/sinking-plan.md: +// an out-of-bounds read walks the prototype chain, which the accessor +// epoch does not cover for writable integer data properties), +// Symbol.iterator, callee, and any value/edge position. args_obj's +// THROW effect keeps it out of generic DCE; this pass, having proven +// every use folded, removes it explicitly. +function sinkArgsObjects( + useMap: UseMap, + fn: Func, + stats: OptStats, + candidates: Inst[] +): boolean { + let changed = false; + for (const alloc of candidates) { + if (!alloc.block) continue; + if (alloc.targets && alloc.targets.length > 0) continue; // block terminator in a try + const reads: Inst[] = []; + let ok = true; + for (const use of usesOf(useMap, alloc)) { + const { inst, index } = use; + if ( + index === 0 && + inst.op === "get_prop_atom" && + inst.imms.atom === "length" && + !inst.targets + ) { + reads.push(inst); + } else { + ok = false; + break; + } + } + if (!ok || reads.length === 0) continue; + const argIndex = alloc.op === "rest_args" ? (alloc.imms.index as number) : 0; + for (const read of reads) { + const al = new Inst(fn, "arg_len", [], { index: argIndex }); + const b = read.block!; + al.block = b; + b.insts.splice(b.insts.indexOf(read), 0, al); + foldRead(useMap, fn, read, al); + stats.reads_folded++; + } + removeInst(useMap, alloc); + stats.args_sunk++; + changed = true; + } + return changed; +} + +// --- direct IIFE inlining ------------------------------------------------------ + +// the desugars (destructuring especially) wrap expression-position work +// in immediately-called closures: make_env / env_store / make_closure / +// call. inlining the call is what exposes the env and the literals +// inside it to the sinking passes above. +// +// conservatively inlinable callee: a single block ending in `return`, +// no frame-dependent ops (arguments/rest/new.target/super), and an +// unused %this param (the IIFE arrows never touch it — lexical `this` +// rides in the env). the call itself must carry no unwind targets. + +const FRAME_OPS = new Set([ + "args_obj", + "rest_args", + "arg_len", + "new_target", + "construct_super", + "construct_super_apply", +]); + +const INLINE_MAX_INSTS = 40; + +function inlinableCallee(m: Module, caller: Func, closure: Inst): Func | null { + const name = closure.imms.fn as string; + const callee = m.functions.find((f) => f.name === name); + if (!callee || callee === caller) return null; + if (callee.blocks.length !== 1) return null; + const entry = callee.entry!; + if (entry.insts.length > INLINE_MAX_INSTS) return null; + const term = entry.terminator; + if (!term || term.op !== "return") return null; + for (const inst of entry.insts) { + if (FRAME_OPS.has(inst.op)) return null; + if (inst.targets && inst !== term) return null; + } + // %this must be unused (we'd otherwise have to reason about the + // runtime's this-coercion on the call path we're deleting) + const thisParam = entry.params[1]; + if (thisParam) { + for (const inst of entry.insts) { + for (const o of inst.operands) if (o === thisParam) return null; + } + } + return callee; +} + +function constUndefinedBefore(fn: Func, before: Inst): Inst { + const c = new Inst(fn, "const", [], { kind: "undefined" }); + const b = before.block!; + c.block = b; + b.insts.splice(b.insts.indexOf(before), 0, c); + return c; +} + +// inline `call` (operands [closure, this, ...args]) by cloning the +// callee's single block in front of it +function inlineCall(fn: Func, call: Inst, closure: Inst, callee: Func): void { + const entry = callee.entry!; + const subst = new Map(); + + // params: [%env, %this, ...declared] -> [closure env, call this, args] + for (let i = 0; i < entry.params.length; i++) { + const p = entry.params[i]!; + let v: Inst; + if (i === 0) v = closure.operands[0]!; + else if (i < call.operands.length) v = call.operands[i]!; + else v = constUndefinedBefore(fn, call); + subst.set(p, v); + } + + const map = (v: Inst): Inst => subst.get(v) || v; + const block = call.block!; + let at = block.insts.indexOf(call); + let result: Inst | null = null; + for (const inst of entry.insts) { + if (inst === entry.terminator) { + result = map(inst.operands[0]!); + break; + } + const clone = new Inst(fn, inst.op, inst.operands.map(map), { ...inst.imms }); + clone.block = block; + block.insts.splice(at++, 0, clone); + subst.set(inst, clone); + } + replaceAllUses(fn, call, result!); + // no live use map here — inlineDirectCalls rebuilds nothing; the + // subsequent passes each build their own + const b = call.block!; + const idx = b.insts.indexOf(call); + if (idx >= 0) b.insts.splice(idx, 1); + call.block = null; +} + +function inlineDirectCalls(m: Module, fn: Func, stats: OptStats): boolean { + const candidates: { call: Inst; closure: Inst; callee: Func }[] = []; + fn.forEachInst((inst) => { + if (inst.op !== "call" || inst.imms.direct || (inst.targets && inst.targets.length > 0)) + return; + const closure = inst.operands[0]!; + if (closure.op !== "make_closure" || closure.block === null) return; + const callee = inlinableCallee(m, fn, closure); + if (callee) candidates.push({ call: inst, closure, callee }); + }); + for (const c of candidates) { + inlineCall(fn, c.call, c.closure, c.callee); + stats.calls_inlined++; + } + return candidates.length > 0; +} + +// --- env scalar replacement ----------------------------------------------------- + +// a make_env whose only uses are base-position env_load/env_store, all +// in the block that allocated it, resolves by a linear walk: each load +// sees the most recent store to its slot (or undefined — env slots +// start undefined, echojs has no TDZ). parent-env chaining stores the +// env in a VALUE position, which classifies as an escape below. +function scalarReplaceEnvs(useMap: UseMap, fn: Func, stats: OptStats): boolean { + let changed = false; + const candidates: Inst[] = []; + fn.forEachInst((inst) => { + if (inst.op === "make_env") candidates.push(inst); + }); + + for (const env of candidates) { + if (!env.block) continue; + let ok = true; + for (const use of usesOf(useMap, env)) { + const { inst, index } = use; + const local = + index === 0 && + (inst.op === "env_load" || inst.op === "env_store") && + inst.block === env.block; + if (!local) { + ok = false; + break; + } + } + if (!ok) continue; + + // linear walk of the defining block. replacements materialize + // after the walk — inserting into insts mid-iteration would + // shift the very array being walked. + const slotValues = new Map(); + const loads: [Inst, Inst | null][] = []; // load -> replacement (null = undefined) + const stores: Inst[] = []; + let started = false; + for (const inst of env.block.insts) { + if (inst === env) { + started = true; + continue; + } + if (!started || inst.operands[0] !== env) continue; + if (inst.op === "env_store") { + slotValues.set(inst.imms.slot as number, inst.operands[1]!); + stores.push(inst); + } else if (inst.op === "env_load") { + loads.push([inst, slotValues.get(inst.imms.slot as number) || null]); + } + } + for (const [load, v] of loads) foldRead(useMap, fn, load, v || constUndefinedBefore(fn, load)); + for (const s of stores) removeInst(useMap, s); + removeInst(useMap, env); + stats.allocs_sunk++; + stats.reads_folded += loads.length; + changed = true; + } + return changed; +} + +// --- iterator-protocol peephole ------------------------------------------------- + +// array destructuring desugars to an iterator walk; over a dense array +// literal the whole chain is compile-time constant: +// +// %a = make_array e0, e1, ... +// %s = get_global atom="Symbol" +// %i = get_prop_atom %s, atom="iterator" +// %f = get_prop %a, %i +// %t = call %f, %a +// %w = call_runtime %t, name="iterator_wrapper_new" +// %g = get_prop_atom %w, atom="getNextValue" +// %v = call %g, %w ; k-th call = element k +// +// the k-th getNextValue call folds to the k-th element (undefined past +// the end — the array iterator yields undefined there). the fold +// assumes the built-in Symbol global and Array.prototype[Symbol.iterator] +// (the desugar already bakes in the former by emitting get_global). +// dense literals only: a hole would read through the prototype chain. +// +// use discipline is strict — every link is consumed only by the next +// (a getRest, an extra array use, a cross-block call, or anything +// carrying unwind targets fails the match), so rest patterns and +// escaping arrays keep the runtime walk. +function foldIteratorWrappers(useMap: UseMap, fn: Func, stats: OptStats): boolean { + let changed = false; + const wrappers: Inst[] = []; + fn.forEachInst((inst) => { + if (inst.op === "call_runtime" && inst.imms.name === "iterator_wrapper_new") + wrappers.push(inst); + }); + + const hasTargets = (i: Inst) => i.targets !== null && i.targets.length > 0; + const soleUse = (v: Inst, user: Inst) => { + const u = usesOf(useMap, v); + return u.length === 1 && u[0]!.inst === user; + }; + + for (const w of wrappers) { + if (!w.block || hasTargets(w)) continue; + + // match the creation chain backwards + const it = w.operands[0]!; + if (it.op !== "call" || it.operands.length !== 2 || it.imms.direct || hasTargets(it)) + continue; + const itfn = it.operands[0]!; + const arr = it.operands[1]!; + if (itfn.op !== "get_prop" || itfn.operands[0] !== arr || hasTargets(itfn)) continue; + const symprop = itfn.operands[1]!; + if (symprop.op !== "get_prop_atom" || symprop.imms.atom !== "iterator" || hasTargets(symprop)) + continue; + const symGlobal = symprop.operands[0]!; + if (symGlobal.op !== "get_global" || symGlobal.imms.atom !== "Symbol") continue; + if (arr.op !== "make_array" || arr.imms.len !== undefined) continue; + if (!soleUse(it, w) || !soleUse(itfn, it) || !soleUse(symprop, itfn)) continue; + if (!usesOf(useMap, arr).every((u) => (u.inst === itfn && u.index === 0) || (u.inst === it && u.index === 1))) + continue; + + // wrapper uses: getNextValue getters + their calls, nothing else + const getters = new Set(); + const calls: Inst[] = []; + let ok = true; + for (const u of usesOf(useMap, w)) { + const i = u.inst; + if ( + i.op === "get_prop_atom" && + i.imms.atom === "getNextValue" && + u.index === 0 && + !hasTargets(i) + ) { + getters.add(i); + } else if ( + i.op === "call" && + i.operands.length === 2 && + u.index === 1 && + !i.imms.direct && + !hasTargets(i) && + i.block === w.block + ) { + calls.push(i); + } else { + ok = false; + break; + } + } + if (!ok || calls.length !== getters.size) continue; + for (const c of calls) if (!getters.has(c.operands[0]!) || !soleUse(c.operands[0]!, c)) ok = false; + if (!ok) continue; + + // k-th call in block order sees element k + calls.sort((a, b) => w.block!.insts.indexOf(a) - w.block!.insts.indexOf(b)); + for (let k = 0; k < calls.length; k++) { + const el = k < arr.operands.length ? arr.operands[k]! : constUndefinedBefore(fn, calls[k]!); + foldRead(useMap, fn, calls[k]!, el); + } + for (const g of getters) removeInst(useMap, g); + removeInst(useMap, w); + removeInst(useMap, it); + removeInst(useMap, itfn); + removeInst(useMap, symprop); + if (usesOf(useMap, symGlobal).length === 0) removeInst(useMap, symGlobal); + // the array itself is now unused (or write-only) — the sinking + // pass and DCE finish it off + stats.iters_folded++; + changed = true; + } + return changed; +} + +// --- unbox/box annihilation --------------------------------------------------- + +// unbox_f64(box_f64(x)) is x: box_f64 always produces a genuinely boxed +// number, so the round-trip is the identity (modulo NaN canonicalization, +// which JS semantics cannot observe — a non-canonical NaN payload only +// ever flows into f64 ops, where any NaN behaves alike, or into a later +// box_f64, which canonicalizes). specialized clones lean on this: formals +// are boxed once at entry and trusted arithmetic re-unboxes them. +function foldUnboxOfBox(fn: Func, stats: OptStats): boolean { + const boxFolds: Inst[] = []; + const constFolds: Inst[] = []; + fn.forEachInst((inst) => { + if (inst.op !== "unbox_f64") return; + const src = inst.operands[0]!; + if (src.op === "box_f64") boxFolds.push(inst); + else if (src.op === "const" && src.imms["kind"] === "number") constFolds.push(inst); + }); + for (const u of boxFolds) { + replaceAllUses(fn, u, u.operands[0]!.operands[0]!); + const b = u.block!; + const idx = b.insts.indexOf(u); + if (idx >= 0) b.insts.splice(idx, 1); + u.block = null; + stats.unbox_folds++; + } + // unbox_f64(const number) is just the raw constant — rewrite the + // unbox in place to f64_const (same Inst object keeps every use) + for (const u of constFolds) { + u.imms = { value: u.operands[0]!.imms["value"] }; + u.op = "f64_const"; + u.operands.length = 0; + stats.unbox_folds++; + } + return boxFolds.length > 0 || constFolds.length > 0; +} + +// --- dead instruction elimination -------------------------------------------- + +// dead-removable: unused results whose computation is unobservable. +// READ|GC effects are fine (a dead read never happens); THROW/WRITE/CALL +// are not — except the literal alloc ops, whose WRITE is to their own +// fresh storage. +function removableWhenDead(inst: Inst): boolean { + if (inst.op === "blockparam") return false; + if (inst.targets && inst.targets.length > 0) return false; + if (inst.op === "make_object" || inst.op === "make_array" || inst.op === "make_object_shaped") + return true; + const info = opInfo(inst.op); + if (info.terminator) return false; + return (info.effects & ~(Effect.READ | Effect.GC)) === 0; +} + +function eliminateDead(fn: Func, stats: OptStats): boolean { + // use counts over operands and edge arguments, indexed by inst.id + const counts = new Array(fn.next_value_id).fill(0); + const bump = (v: Inst) => { + counts[v.id] = (counts[v.id] ?? 0) + 1; + }; + fn.forEachInst((inst) => { + for (const o of inst.operands) bump(o); + if (inst.targets) { + for (const t of inst.targets) for (const a of t.args) if (a) bump(a); + } + }); + + const worklist: Inst[] = []; + fn.forEachInst((inst) => { + if (!counts[inst.id] && removableWhenDead(inst)) worklist.push(inst); + }); + + let changed = false; + while (worklist.length > 0) { + const inst = worklist.pop()!; + if (!inst.block) continue; + const b = inst.block; + const idx = b.insts.indexOf(inst); + if (idx >= 0) b.insts.splice(idx, 1); + inst.block = null; + stats.dead_removed++; + // a shaped alloc reaching DCE means its reads/guards all folded + // (or it was never consumed) — that IS the sink completing + if (inst.op === "make_object_shaped") stats.shape_allocs_sunk++; + changed = true; + for (const o of inst.operands) { + const n = --counts[o.id]!; + if (n === 0 && o.block && removableWhenDead(o)) worklist.push(o); + } + } + return changed; +} + +// --- driver ------------------------------------------------------------------- + +export function optimizeFunction( + fn: Func, + module?: Module, + stats?: OptStats, + // the module's stable %self slots (computeStableSlots), for slot-load + // CSE. optimizeModule computes and threads this; standalone callers + // (tests) may omit it — CSE then runs block-local only. + stableSlots?: Set +): OptStats { + const s = stats || newStats(); + const flags = readSinkFlags(); + // to fixpoint: inlining an IIFE exposes its env and literals; + // sinking an outer literal can un-escape one nested inside it (its + // only use was as the outer's operand) + let rounds = 0; + for (;;) { + let changed = module ? inlineDirectCalls(module, fn, s) : false; + if (eliminateDead(fn, s)) changed = true; // kill the closure before judging its env + // one scan per round (use map + every sink pass's candidates), + // kept accurate by the mutation helpers + const scan = scanRound(fn); + const useMap = scan.useMap; + if (scalarReplaceEnvs(useMap, fn, s)) changed = true; + if (foldIteratorWrappers(useMap, fn, s)) changed = true; + if (sinkAllocations(useMap, fn, module, s, flags.noShaped, scan.objAllocs, scan.shapedAllocs)) + changed = true; + if (!flags.noArgs && sinkArgsObjects(useMap, fn, s, scan.argsAllocs)) changed = true; + // flow-sensitive sinking (written / partially-escaping + // candidates). Runs LAST in the round sharing the same scan — + // it folds guard branches and mints join params, so nothing + // after it may consult the map this round + if ( + !flags.noFlow && + sinkFlowAllocations(fn, module, s, useMap, scan.objAllocs, scan.shapedAllocs) + ) + changed = true; + // shaped sinking folds guard branches; reclaim the dead arms so + // the next round's use map lets the alloc itself drain + if (sweepUnreachableBlocks(fn)) changed = true; + if (eliminateDead(fn, s)) changed = true; + if (!changed || ++rounds > 10) break; + } + // module-slot load CSE runs BEFORE the region passes: a toplevel + // receiver reloaded per access is a distinct SSA value per region, + // and receiver identity is exactly what lets adjacent shape regions + // merge (the shapes-P3 note). + if (!flags.noCse && cseModuleSlotLoads(fn, stableSlots, s)) eliminateDead(fn, s); + // guard-region passes over the --types diamonds. They run + // after the general fixpoint (env scalarization has exposed the SSA + // values the diamonds guard) and bail immediately when lowering + // emitted no number guards — every flag-off compile. + if (optimizeGuardRegions(fn, s)) eliminateDead(fn, s); + // shape-guard region merging + fact folding (bails + // immediately without has_shape guards — every flag-off compile). + // a short fixpoint with rawJoinParams — heterogeneous merges + // expose raw joins, and a raw join linearizes a fast side the next + // shape-region match can grow through. + for (let i = 0; i < 8; i++) { + let ch = false; + if (optimizeShapeRegions(fn, module, s)) { + eliminateDead(fn, s); + ch = true; + } + if (rawJoinParams(fn, s)) { + eliminateDead(fn, s); + ch = true; + } + if (!ch) break; + } + // unbox/boolean-join cleanups. These run AFTER the guard-region passes: the + // merge machinery pattern-matches diamond fast arms (unbox of the + // guarded value / of a literal const), so annihilating round-trips + // or rewriting const unboxes earlier would refuse valid merges. + if (foldUnboxOfBox(fn, s)) eliminateDead(fn, s); + if (threadBooleanJoins(fn, s)) eliminateDead(fn, s); + // the compiler-P1 cleanup passes (cleanup.ts): constant folding, + // trivial params, to_boolean/typeof elimination, lattice-typed f64 + // lowering. They run LAST for the same reason foldUnboxOfBox does: + // folding arithmetic earlier would perturb the exact IR shapes the + // region matchers verify. + if (!flags.noCleanup && cleanupFunction(fn, s)) { + foldUnboxOfBox(fn, s); + eliminateDead(fn, s); + } + return s; +} + +// exposed for the module-level passes (devirt.ts) that delete uses and +// want their dead operands swept without a full optimizer run +export function eliminateDeadInFunction(fn: Func, stats?: OptStats): boolean { + return eliminateDead(fn, stats || newStats()); +} + +export function optimizeModule(m: Module, toplevelName?: string): OptStats { + const stats = newStats(); + const stableSlots = + toplevelName !== undefined ? computeStableSlots(m.functions, toplevelName) : undefined; + for (const fn of m.functions) optimizeFunction(fn, m, stats, stableSlots); + return stats; +} diff --git a/lib/eir/oracle.ts b/lib/eir/oracle.ts new file mode 100644 index 00000000..0e662ab5 --- /dev/null +++ b/lib/eir/oracle.ts @@ -0,0 +1,466 @@ +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: + */ + +// The MAAM type oracle. With --types on, compile() +// hands us the desugared toplevel BEFORE collectEIRToplevel consumes it; +// we wrap its body as a Program (preserving node identity — the oracle +// is keyed on the exact node objects), run the echojs-maam abstract +// interpreter over it, log its stats, and return a TypeOracle +// over the result. Lowering consumes it only under --types; +// --types-dump prints per-binding types for hand-checking. +// +// Two hard rules, both load-bearing: +// - maam is require()d lazily, only when the probe actually runs, so +// a flag-off compile never touches it (and never pays for it); +// - every failure here — missing/unbuilt submodule, an analysis +// error, a self-hosted compiler with no host require() — degrades +// to a compiler warning. --types must never turn a compiling +// program into a failing one. + +import * as path from "@node-compat/path"; +import * as fs from "@node-compat/fs"; +import type * as e from "../estree"; +import { reportWarning } from "../errors"; +import * as commonIds from "../common-ids"; + +// The slice of maam's AnalysisResult the probe consumes, typed +// structurally so we never import (or resolve types from) the +// submodule itself. +interface MaamMetrics { + reachedStates: number; + configs: number; + iterations: number; + shapesInterned: number; + unknownCalls: number; + // added alongside this integration; absent in older builds + degradedBindings?: number; + stateCapHits?: number; + stateCapFuncs?: number; + shapeCapHits?: number; +} + +// the slice of maam's Shape the shape queries consume (structural) +interface MaamShape { + id: number; + fields: ReadonlyArray<{ name: string; type: string }>; + megamorphic?: boolean; +} + +interface MaamResult { + metrics: MaamMetrics; + describe(): string; + warnings(): Array<{ kind: string }>; + // node-identity oracle: joined TypeSig ("num", "num|str", "⊤", …) + // for the exact node object, undefined for unreached/unmapped nodes. + typeOfNode(n: unknown): string | undefined; + // node-identity shape queries; absent in older maam + // builds (the oracle degrades to "no shape facts", never errors) + receiverShapesOfNode?(n: unknown): MaamShape[] | undefined; + fieldOrderOfShape?(s: MaamShape): readonly string[] | undefined; +} + +interface MaamModule { + analyze(program: unknown, spec: unknown): MaamResult; + kCFA(...args: unknown[]): unknown; +} + +const MAAM_DIST_REL = ["external-deps", "echojs-maam", "dist", "cjs", "index.js"]; +const MAAM_SUBMODULE_REL = ["external-deps", "echojs-maam"]; + +// undefined = not attempted yet; null = attempted and unavailable (the +// warning has already been issued — don't repeat it per module) +let cached_maam: MaamModule | null | undefined; + +function errorMessage(err: unknown): string { + if (err instanceof Error) return `${err.name}: ${err.message}`; + return String(err); +} + +// Locate and require() the maam CJS build. We walk up from this +// module's directory looking for external-deps/echojs-maam — that works +// both for the source tree (lib/eir/) and for the babel'd node tree +// (lib/generated/lib/eir/, whose ancestors include the repo root). A +// self-hosted (stage1+) compiler has no host require()/__dirname; the +// probe is a documented no-op-with-a-warning there. +function loadMaam(source_filename: string): MaamModule | null { + if (cached_maam !== undefined) return cached_maam; + cached_maam = null; + + if (typeof require !== "function" || typeof __dirname !== "string") { + reportWarning( + "--types is not available in a self-hosted compiler (no host require()); type analysis skipped.", + source_filename + ); + return null; + } + + let submodule_dir: string | null = null; + for (let dir = __dirname, prev = ""; dir !== prev; prev = dir, dir = path.dirname(dir)) { + const sub = path.join(dir, ...MAAM_SUBMODULE_REL); + if (!fs.existsSync(sub)) continue; + submodule_dir = sub; + const dist = path.join(dir, ...MAAM_DIST_REL); + if (!fs.existsSync(dist)) break; // submodule present, build output missing + try { + cached_maam = require(dist) as MaamModule; + return cached_maam; + } catch (err) { + reportWarning( + `--types: failed to load echojs-maam from ${dist} (${errorMessage(err)}); type analysis skipped.`, + source_filename + ); + return null; + } + } + + reportWarning( + submodule_dir !== null + ? `--types: echojs-maam is present at ${submodule_dir} but its CJS build is missing; ` + + "run `npm run build && npm run build:cjs` there. Type analysis skipped." + : "--types: could not locate the external-deps/echojs-maam submodule; type analysis skipped.", + source_filename + ); + return null; +} + +function warningSummary(warnings: Array<{ kind: string }>): string { + if (warnings.length === 0) return "none"; + const counts = new Map(); + for (const w of warnings) counts.set(w.kind, (counts.get(w.kind) || 0) + 1); + return [...counts.entries()].map(([kind, n]) => `${kind}:${n}`).join(","); +} + +// --- the TypeOracle contract -- + +export type TypeTag = "number" | "string" | "boolean" | "undefined" | "null" | "object" | "closure"; + +export interface EirType { + // "top" = no information + tags: ReadonlySet | "top"; +} + +// one field of a receiver's shape, in insertion order. +// repr mirrors the runtime's EJSShapeRepr: "f64" iff the field's TypeSig is +// exactly "num" (the runtime classifies stored values the same way), else +// "boxed" — and a sig whose union straddles the num/non-num line has no +// determined repr, so the whole query declines (guard identity needs every +// field's repr, not just the accessed one). +export interface OracleShapeField { + name: string; + repr: "boxed" | "f64"; +} + +export type ShapeDeclineReason = + | "unmapped" // node unknown to the analysis (or maam predates the query) + | "polymorphic" // more terminal shapes than the guard budget (>2) + | "megamorphic" // the ⊤ shape + | "capped" // shapeCapHits > 0: some shape set was widened this module + | "union-repr" // a field's TypeSig straddles num/non-num + | "no-order" // no ordered witness for the shape + | "empty"; // the empty shape (nothing to access) + +// a query answer carries ONE OR TWO exact shapes. Two +// shapes is the measured 2-way polymorphic extension — every shape in the +// answer independently passes the full exactness screen (non-megamorphic, +// non-empty, ordered witness, single-tag reprs); a set where ANY member +// falls short declines the whole site (criterion 2 — no near-misses), +// and >2 declines "polymorphic" as before. +export type ShapeQuery = + | { shapes: OracleShapeField[][]; declined?: undefined } + | { declined: ShapeDeclineReason; shapes?: undefined }; + +export interface TypeOracle { + // type of the value an expression node evaluates to (join over all + // reached contexts); "top" when unknown/unanalyzed + typeOfNode(n: e.Node): EirType; + // the receiver-shape facts for a property + // access's object node — exact facts only (non-megamorphic, uncapped, + // all reprs single-tag, ordered witness present), at most two shapes + // (the polymorphic-chain budget), everything else a counted decline. + // Optional so stub oracles predating shapes keep working; absent = + // no shape facts. + receiverShapeOfNode?(n: e.Node): ShapeQuery; + // required before any UNguarded consumption (guarded fast paths don't + // need it) + closedWorld(): boolean; + describe(): string; // stats line for --types logging +} + +// what the probe actually returns: the oracle plus query telemetry. The +// `unknown` counter is the node-identity canary — a query for a node maam +// never saw (dead code, unmapped glue, or a node MINTED AFTER the probe, +// e.g. by normalizeDefaultExports' splicing) reads as "top"; if identity +// ever silently breaks at scale, this number says so. +export interface ProbeOracleStats { + queries: number; + unknown: number; +} + +export interface ProbeOracle extends TypeOracle { + readonly stats: ProbeOracleStats; +} + +const TAG_BY_SIG: Record = { + num: "number", + str: "string", + bool: "boolean", + undefined: "undefined", + null: "null", + obj: "object", + fn: "closure", +}; + +// Map a maam TypeSig ("num", "num|str", "⊤", "never", …) to an EirType. +// Anything we do not positively recognize — including a missing sig and any +// unrecognized constituent a future maam might add — is "top": the oracle +// never guesses. Exported for the unit tests in lib/eir/tests.ts. +export function typeSigToEirType(sig: string | undefined): EirType { + if (sig === undefined || sig === "⊤" || sig === "never") return { tags: "top" }; + const tags = new Set(); + for (const part of sig.split("|")) { + const tag = TAG_BY_SIG[part]; + if (tag === undefined) return { tags: "top" }; + tags.add(tag); + } + return { tags }; +} + +// Map a maam field TypeSig to a runtime shape repr, or null when the sig +// straddles the num/non-num line (no single runtime repr exists — the +// object flips shapes at runtime and no one guard can be monomorphic). +// The runtime's classify_repr is EJSVAL_IS_NUMBER ? F64 : BOXED, so any +// union of non-num tags is uniformly BOXED. Exported for unit tests. +export function typeSigToShapeRepr(sig: string): "boxed" | "f64" | null { + if (sig === "num") return "f64"; + const parts = sig.split("|"); + for (const part of parts) { + if (part === "num" || TAG_BY_SIG[part] === undefined) return null; + } + return "boxed"; +} + +// The common-ids singleton identifier nodes (ONE object each, spliced into +// many sites by the desugar passes). Node-identity oracle queries on them +// would be ambiguous; the dump skips them outright (maam's ambiguity poison +// backstops any that slip through elsewhere). +let cached_singletons: Set | null = null; +function singletonIdentifiers(): Set { + if (!cached_singletons) { + cached_singletons = new Set( + Object.values(commonIds).filter( + (v) => typeof v === "object" && v !== null && (v as { type?: string }).type === "Identifier" + ) + ); + } + return cached_singletons; +} + +// Collect the DECLARATION-site binding identifiers of the wrapped program: +// variable-declarator ids (incl. pattern leaves), function-declaration names, +// and parameters (identifiers, pattern leaves, rest — both the RestElement +// and old-dialect `rest`-field forms). These are minted per-site by the +// parser/desugars, so they are safe node-identity keys — except the +// singletons and %-named internals, which are skipped, and any node object +// encountered twice, which is a splice and skipped too. +function collectDeclarationIds(program: { body: e.Statement[] }): e.Identifier[] { + const out: e.Identifier[] = []; + const patternLeaves = (p: unknown): void => { + if (!p || typeof p !== "object") return; + const node = p as { type?: string }; + switch (node.type) { + case "Identifier": + out.push(node as e.Identifier); + return; + case "ObjectPattern": + for (const prop of (node as unknown as { properties: unknown[] }).properties) { + const pr = prop as { type?: string; value?: unknown; argument?: unknown }; + if (pr.type === "Property") patternLeaves(pr.value); + else patternLeaves(pr.argument); + } + return; + case "ArrayPattern": + for (const el of (node as unknown as { elements: unknown[] }).elements) patternLeaves(el); + return; + case "AssignmentPattern": + return patternLeaves((node as unknown as { left: unknown }).left); + case "RestElement": + case "SpreadElement": + return patternLeaves((node as unknown as { argument: unknown }).argument); + default: + return; + } + }; + const walk = (n: unknown): void => { + if (!n || typeof n !== "object") return; + if (Array.isArray(n)) { + for (const x of n) walk(x); + return; + } + const node = n as Record & { type?: string }; + if (typeof node.type === "string") { + if (node.type === "VariableDeclarator") patternLeaves(node.id); + else if (node.type === "FunctionDeclaration" && node.id) patternLeaves(node.id); + if ( + node.type === "FunctionDeclaration" || + node.type === "FunctionExpression" || + node.type === "ArrowFunctionExpression" + ) { + for (const p of (node.params as unknown[]) ?? []) patternLeaves(p); + if (node.rest) patternLeaves(node.rest); // old-dialect rest field + } + } + for (const key of Object.keys(node)) { + if (key === "loc" || key === "range") continue; + walk(node[key]); + } + }; + walk(program); + + const singletons = singletonIdentifiers(); + const seen = new Set(); + const dupes = new Set(); + for (const ident of out) { + if (seen.has(ident)) dupes.add(ident); // spliced node: ambiguous key + seen.add(ident); + } + return out.filter( + (ident) => !dupes.has(ident) && !singletons.has(ident) && !ident.name.startsWith("%") + ); +} + +function locOf(ident: e.Identifier): { line: number; col: number } | null { + const loc = (ident as { loc?: { start?: { line: number; column: number } } }).loc; + if (loc && loc.start) return { line: loc.start.line, col: loc.start.column + 1 }; + return null; +} + +// Print one line per declaration-site binding: name, source position when the +// node has one (synthetic desugar nodes may not), and the raw maam TypeSig +// ("(unmapped→⊤)" for nodes the analysis never saw). Sorted by position, +// synthetic nodes last, ties broken by name then collection order. +function dumpBindingTypes( + result: MaamResult, + program: { body: e.Statement[] }, + source_filename: string +): void { + const ids = collectDeclarationIds(program); + const rows = ids.map((ident, index) => ({ ident, index, loc: locOf(ident) })); + rows.sort((a, b) => { + if (a.loc && b.loc) + return a.loc.line - b.loc.line || a.loc.col - b.loc.col || + a.ident.name.localeCompare(b.ident.name) || a.index - b.index; + if (a.loc) return -1; + if (b.loc) return 1; + return a.ident.name.localeCompare(b.ident.name) || a.index - b.index; + }); + for (const row of rows) { + const sig = result.typeOfNode(row.ident); + const where = row.loc ? `${row.loc.line}:${row.loc.col}` : "synthetic"; + console.warn( + `--types-dump: ${source_filename}: ${row.ident.name} @${where} : ${sig ?? "(unmapped→⊤)"}` + ); + } +} + +// Run the probe over the module's desugared tree. `tree` is the +// post-pre_eir_convert Program whose body[0] is the synthetic toplevel +// FunctionDeclaration (insert_toplevel_func) holding the module's +// statements. Returns a TypeOracle over the analysis (so compile() can +// thread it onward), or null when anything degraded; callers +// must treat null as "no type information", never as an error. +export function runTypeAnalysisProbe( + tree: e.Program, + source_filename: string, + dump = false +): ProbeOracle | null { + const maam = loadMaam(source_filename); + if (!maam) return null; + + const toplevel = tree.body[0]; + if (!toplevel || toplevel.type !== "FunctionDeclaration") { + reportWarning( + "--types: expected the synthetic toplevel FunctionDeclaration; type analysis skipped.", + source_filename + ); + return null; + } + + // Same body array, same node objects — no cloning. + const program = { type: "Program", sourceType: "script", body: toplevel.body.body }; + + const started = Date.now(); + try { + const result = maam.analyze( + program, + maam.kCFA(1, "flow-sensitive", "call-site", /*shapeCap*/ 64, false, false, false, /*stateCap*/ 512) + ); + const wall = Date.now() - started; + const m = result.metrics; + const statsLine = + `--types: ${source_filename}: wall=${wall}ms reachedStates=${m.reachedStates} ` + + `configs=${m.configs} iterations=${m.iterations} shapesInterned=${m.shapesInterned} ` + + `unknownCalls=${m.unknownCalls} degradedBindings=${m.degradedBindings ?? 0} ` + + `stateCapHits=${m.stateCapHits ?? 0} stateCapFuncs=${m.stateCapFuncs ?? 0} ` + + `shapeCapHits=${m.shapeCapHits ?? 0} warnings=${warningSummary(result.warnings())}`; + console.warn(statsLine); + console.warn(result.describe()); + if (dump) dumpBindingTypes(result, program as { body: e.Statement[] }, source_filename); + + const stats: ProbeOracleStats = { queries: 0, unknown: 0 }; + return { + stats, + typeOfNode: (n) => { + stats.queries++; + const sig = result.typeOfNode(n); + if (sig === undefined) stats.unknown++; + return typeSigToEirType(sig); + }, + // exact receiver-shape facts, every + // near-miss a counted decline (promotion criterion 2 — no + // near-misses). Up to TWO shapes survive (the poly + // budget); each must pass the full screen independently. + receiverShapeOfNode: (n): ShapeQuery => { + if (!result.receiverShapesOfNode || !result.fieldOrderOfShape) + return { declined: "unmapped" }; // older maam build + if ((m.shapeCapHits ?? 0) > 0) return { declined: "capped" }; + const shapes = result.receiverShapesOfNode(n); + if (shapes === undefined || shapes.length === 0) + return { declined: "unmapped" }; + if (shapes.length > 2) return { declined: "polymorphic" }; + const out: OracleShapeField[][] = []; + for (const s of shapes) { + if (s.megamorphic) return { declined: "megamorphic" }; + if (s.fields.length === 0) return { declined: "empty" }; + const order = result.fieldOrderOfShape(s); + if (!order || order.length !== s.fields.length) + return { declined: "no-order" }; + const typeByName = new Map(s.fields.map((f) => [f.name, f.type])); + const fields: OracleShapeField[] = []; + for (const name of order) { + const sig = typeByName.get(name); + if (sig === undefined) return { declined: "no-order" }; + const repr = typeSigToShapeRepr(sig); + if (repr === null) return { declined: "union-repr" }; + fields.push({ name, repr }); + } + out.push(fields); + } + return { shapes: out }; + }, + // The plan text gates closedWorld() on unknownCalls alone because it + // predates the degradedBindings counter (unmodeled imports, rest + // params — Chunks A/D). Both must be zero: either one means some + // value in the store is a stand-in, not a fact. + closedWorld: () => m.unknownCalls === 0 && (m.degradedBindings ?? 0) === 0, + describe: () => statsLine, + }; + } catch (err) { + const wall = Date.now() - started; + reportWarning( + `--types: analysis failed after ${wall}ms (${errorMessage(err)}); continuing without type information.`, + source_filename + ); + return null; + } +} diff --git a/lib/eir/printer.ts b/lib/eir/printer.ts new file mode 100644 index 00000000..3b90849d --- /dev/null +++ b/lib/eir/printer.ts @@ -0,0 +1,111 @@ +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: + */ + +// canonical textual form of EIR. deterministic (values numbered densely in +// print order) so it can back golden tests. + +import { opInfo } from "./ops"; +import type { Func, Inst, Module, Target, ImmValue } from "./ir"; + +function fmtImm(v: ImmValue): string { + if (typeof v === "string") return JSON.stringify(v); + if (Array.isArray(v)) return `[${v.map(fmtImm).join(", ")}]`; + return String(v); +} + +type NameOf = (v: Inst | null | undefined) => string; + +export function printFunction(fn: Func): string { + // dense renumbering in block/instruction order for stable output + const names = new Map(); + let next = 0; + const nameOf: NameOf = (v) => { + if (v === null || v === undefined) return ""; + let name = names.get(v); + if (name === undefined) { + name = `%${next++}`; + names.set(v, name); + } + return name; + }; + + for (const b of fn.blocks) { + for (const p of b.params) nameOf(p); + for (const i of b.insts) nameOf(i); + } + + const lines: string[] = []; + const header_params = fn.entry ? fn.entry.params.map((p) => `${nameOf(p)}: ${p.type}`) : []; + // sigged clones print their result type; un-sigged + // functions keep the existing byte-identical header + const result = fn.sig && fn.sig.result !== "any" ? `: ${fn.sig.result}` : ""; + lines.push(`fn @${fn.name}(${header_params.join(", ")})${result} {`); + + const paramStr = (p: Inst) => `${nameOf(p)}: ${p.type}`; + + for (const b of fn.blocks) { + if (b === fn.entry) { + lines.push(`^${b.name}:`); + } else { + const inner = b.params.map(paramStr).join(", "); + lines.push(`^${b.name}(${inner}):`); + } + + for (const inst of b.insts) { + lines.push(` ${printInst(inst, nameOf)}`); + } + } + lines.push("}"); + return lines.join("\n"); +} + +function printTarget(t: Target, nameOf: NameOf): string { + const args = t.args.map((a) => nameOf(a)).join(", "); + const kind = t.kind ? `${t.kind} ` : ""; + return `${kind}^${t.block.name}(${args})`; +} + +export function printInst(inst: Inst, nameOf: NameOf): string { + const info = opInfo(inst.op); + + const producesValue = + inst.op !== "br" && + inst.op !== "cond_br" && + inst.op !== "return" && + inst.op !== "throw" && + inst.op !== "unreachable"; + + const operand_strs = inst.operands.map((o) => nameOf(o)); + const imm_strs: string[] = []; + if (info.imms) { + for (const imm of info.imms) { + if (inst.imms[imm] !== undefined) imm_strs.push(`${imm}=${fmtImm(inst.imms[imm])}`); + } + } + + const all = operand_strs.concat(imm_strs); + let text = inst.op + (all.length ? " " + all.join(", ") : ""); + + if (inst.targets && inst.targets.length > 0) { + text += " -> " + inst.targets.map((t) => printTarget(t, nameOf)).join(", "); + } + + // typed defs (the low tier) print their type; "any" stays bare so all + // existing output is byte-identical + if (producesValue) + return inst.type === "any" + ? `${nameOf(inst)} = ${text}` + : `${nameOf(inst)}: ${inst.type} = ${text}`; + return text; +} + +export function printModule(mod: Module): string { + const out = [`module ${mod.name} {`]; + for (const fn of mod.functions) { + out.push(printFunction(fn)); + out.push(""); + } + out.push("}"); + return out.join("\n"); +} diff --git a/lib/eir/scopes.ts b/lib/eir/scopes.ts new file mode 100644 index 00000000..1a3f68f9 --- /dev/null +++ b/lib/eir/scopes.ts @@ -0,0 +1,1031 @@ +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: + */ + +// Scope analysis for EIR lowering: +// +// - every binding (param, var/let/const, function decl, catch param) gets +// a unique id, so shadowing never aliases SSA variables; +// - every Identifier reference is resolved to its binding (or to null, +// meaning "global") in a side map keyed by AST node; +// - a binding referenced from a function nested below its declaration is +// marked captured and assigned an environment slot in the declaring +// function; functions learn their env size and whether their env must +// carry a parent-env pointer in slot 0. +// +// The walker deliberately covers the same whitelisted AST subset as +// lower.ts and throws LowerNotSupported on anything else, early — a +// construct that doesn't lower is a compile error, and it must surface +// before lowering starts mutating the module. + +import type * as e from "../estree"; +import { LowerNotSupported } from "./errors"; +import { eir_intrinsics } from "./intrinsics"; +import type { BinaryOperator } from "../estree"; + +// compound assignment operator -> the binary operator it desugars to +// (kept in sync with lower.ts's binops table) +export const compound_assign_ops = { + "+=": "+", + "-=": "-", + "*=": "*", + "/=": "/", + "%=": "%", + "&=": "&", + "|=": "|", + "^=": "^", + "<<=": "<<", + ">>=": ">>", + ">>>=": ">>>", +} satisfies Record as Record; + +export type BindingKind = "param" | "local" | "fn" | "catch" | "self" | "this"; + +let binding_id_gen = 0; + +export class Binding { + name: string; + uid: string; + kind: BindingKind; + fnInfo: FnInfo | null; // declaring FnInfo + captured = false; + slot = -1; // env slot, if captured + loopEnv: LoopEnv | null = null; // LoopEnv candidate, for let/const loop bindings + + constructor(name: string, kind: BindingKind, fnInfo: FnInfo | null) { + this.name = name; + this.uid = `${name}#${binding_id_gen++}`; + this.kind = kind; + this.fnInfo = fnInfo; + } +} + +export class FnInfo { + // discriminant against LoopEnv in env-descriptor chains (lower.ts) + readonly isLoopEnv = false as const; + // set by lowering (lower.ts lowerOneFunction) + lowered = false; + fn: import("./ir").Func | null = null; + node: e.Function; + name: string; + parent: FnInfo | null; + children: FnInfo[] = []; + params: Binding[] = []; + bindings: Binding[] = []; // every Binding declared here + needsParentEnv = false; // some descendant reaches past this fn + envSize = 0; // slots (incl. parent slot), 0 = no env + parentSlot = -1; // slot holding the parent env, or -1 + creationLoopEnv: LoopEnv | null = null; // innermost LoopEnv at the definition site + // set lazily by the walker + restBinding: Binding | null = null; + defaults: (e.Expression | null)[] = []; + argumentsBinding: Binding | null = null; + usesArguments = false; + thisBinding: Binding | null = null; + isToplevel = false; + + constructor(node: e.Function, name: string, parent: FnInfo | null) { + this.node = node; + this.name = name; + this.parent = parent; + if (parent) parent.children.push(this); + } +} + +// a per-iteration environment for a loop whose let/const bindings are +// captured by closures (`for (let i ...) { use(() => i); }`): each +// iteration allocates a fresh env so every closure sees that iteration\'s +// binding. slot 0 always holds the enclosing environment (the value of +// curEnv at loop entry). candidates are created for every let/const +// loop declaration during the walk and materialize after it, once +// capture flags are known; unmaterialized candidates are transparent. +let loopenv_id_gen = 0; + +export class LoopEnv { + id: number; + readonly isLoopEnv = true as const; + fnInfo: FnInfo | null; // the function containing the loop + node: e.Node; // the loop AST node + parentCandidate: LoopEnv | null; // enclosing LoopEnv in the same fn, or null + allBindings: Binding[] = []; // every let/const binding the loop declares + bindings: Binding[] = []; // the captured subset (set at materialization) + materialized = false; + envSize = 0; + parentSlot = -1; // always 0 once materialized + + constructor(fnInfo: FnInfo | null, node: e.Node, parentCandidate: LoopEnv | null) { + this.id = loopenv_id_gen++; + this.fnInfo = fnInfo; + this.node = node; + this.parentCandidate = parentCandidate; + } +} + +class LexScope { + parent: LexScope | null; + fnInfo: FnInfo | null; + names = new Map(); + isFnTop = false; + + constructor(parent: LexScope | null, fnInfo: FnInfo | null) { + this.parent = parent; + this.fnInfo = fnInfo; + } + + declare(name: string, kind: BindingKind): Binding { + // redeclaration in the same lexical scope reuses the binding (var x; + // var x; — and function-level var hoisting lands them in one scope) + const existing = this.names.get(name); + if (existing) return existing; + const binding = new Binding(name, kind, this.fnInfo); + this.names.set(name, binding); + this.fnInfo!.bindings.push(binding); + return binding; + } + + lookup(name: string): Binding | null { + let s: LexScope | null = this; + while (s) { + const found = s.names.get(name); + if (found) return found; + s = s.parent; + } + return null; + } +} + +export interface LabelEntry { + name: string; + isLoop: boolean; +} + +export class ScopeAnalysis { + refs = new Map(); // Identifier node -> Binding | null (global) + fnInfos = new Map(); + globalNames = new Set(); // free names that resolved to nothing + globalValueNames = new Set(); // free names used other than as a direct callee + globalAssignedNames = new Set(); // free names that are assigned to + anon_gen = 0; + curScope: LexScope | null = null; + curFn: FnInfo | null = null; + // per-iteration loop env candidates: every let/const loop + // declaration gets one; those with captured bindings materialize + // after the walk (see analyzeFunction) and lowering builds a + // fresh env per iteration. + loopEnvs: LoopEnv[] = []; + loopEnvStack: LoopEnv[] = []; // active candidates (innermost last) + loopEnvByNode = new Map(); // loop AST node -> head LoopEnv + bodyEnvByNode = new Map(); // loop AST node -> body LoopEnv + // set around a for-init declaration walk so the declared bindings + // attach to the loop's env candidate + pendingLoopEnv: LoopEnv | null = null; + // labels are per-function (a labeled break can't cross a function + // boundary); enterFunction/leaveFunction save and restore + labelStack: LabelEntry[] = []; + savedLabelStacks: LabelEntry[][] = []; + // toplevel-as-EIR mode (analyzeToplevel): module-scope names backed + // by module slots (or const-literal folds). declarations of these + // at the root function's top level create NO local binding — every + // reference resolves as free and the integration's refs machinery + // routes it through the slot. + moduleSlotNames: Set | null = null; + rootInfo: FnInfo | null = null; + // every EIR function name handed out by enterFunction (scope + // qualification alone isn't unique) + usedFnNames = new Set(); + + // the loop's materialized head env, or null (for lowering) + loopEnvOf(node: e.Node): LoopEnv | null { + let le = this.loopEnvByNode.get(node); + return le && le.materialized ? le : null; + } + + // the loop's materialized body env, or null (for lowering) + loopBodyEnvOf(node: e.Node): LoopEnv | null { + let le = this.bodyEnvByNode.get(node); + return le && le.materialized ? le : null; + } + + resolve(node: e.Node): Binding | null | undefined { + return this.refs.get(node); + } + + infoFor(fnNode: e.Function): FnInfo | undefined { + return this.fnInfos.get(fnNode); + } + + // --- entry point --------------------------------------------------------- + + // walk a function's body without pushing a block scope: the body's + // top-level declarations belong to the function scope itself (isFnTop), + // otherwise every body-level function declaration would look like a + // block-level one. + walkFnBody(body: e.BlockStatement): void { + // hoisting, pass 1: function-scope declarations are visible from + // the top of the function regardless of statement order (function + // declarations hoist, and echojs's no-TDZ let/const read as + // undefined before their statement). without this, a nested + // function placed ABOVE a let/const it captures — which the + // pre-EIR HoistFuncDecls pass produces routinely — resolved the + // name as a global. + for (const s of body.body) { + let stmt: e.Statement = s; + if (stmt.type === "ExportNamedDeclaration" && stmt.declaration) + stmt = stmt.declaration; + if (stmt.type === "VariableDeclaration") { + for (let d of stmt.declarations) { + // patterns are pre-desugared (DesugarDestructuring runs + // before HoistFuncDecls); if one reaches us anyway, + // fall back rather than silently skip its targets — + // they'd misresolve as globals from any hoisted + // function above the declaration + if (d.id.type !== "Identifier") + throw LowerNotSupported( + `fn-top declaration pattern ${d.id.type}`, + stmt.loc + ); + if (this.slotBackedDecl(d.id.name, this.curScope!)) continue; + this.curScope!.declare(d.id.name, "local"); + } + } else if (stmt.type === "FunctionDeclaration" && stmt.id) { + if (this.slotBackedDecl(stmt.id.name, this.curScope!)) continue; + this.curScope!.declare(stmt.id.name, "fn"); + } else { + // `var`s nested in other statements (`if (c) var x = ...`) + // hoist to the function scope too + this.prescanNestedVars(stmt); + } + } + for (let s of body.body) this.walkStmt(s); + } + + // pre-declare var-kind declarations at any statement depth (stopping + // at nested functions, whose vars are their own) + prescanNestedVars(n: unknown): void { + if (!n || typeof n !== "object") return; + if (Array.isArray(n)) { + for (const el of n) this.prescanNestedVars(el); + return; + } + const node = n as e.Node; + switch (node.type) { + case "FunctionDeclaration": + case "FunctionExpression": + case "ArrowFunctionExpression": + return; // function boundary + case "VariableDeclaration": + if (node.kind !== "var") return; // let/const are block-scoped + for (const d of node.declarations) { + if (d.id.type !== "Identifier") + throw LowerNotSupported( + `nested var declaration pattern ${d.id.type}`, + node.loc + ); + if (this.slotBackedDecl(d.id.name, this.curScope!)) continue; + this.curScope!.declare(d.id.name, "local"); + } + return; + default: + for (const k of Object.keys(node)) { + if (k === "loc") continue; + this.prescanNestedVars((node as unknown as Record)[k]); + } + return; + } + } + + analyzeFunction(fnNode: e.Function, name?: string): FnInfo { + // bind the function's own name outside its scope (like a named + // function expression) so recursion resolves to a "self" binding + // instead of looking like a global; lowering turns calls through + // it into direct calls. + // for an anonymous function-expression candidate (var f = + // function () {}), the module-scope name serves as the self name: + // integration only makes such a candidate viable when the name is + // never reassigned. + let selfName = (fnNode.id && fnNode.id.name) || name; + let selfBinding = null; + if (selfName) { + this.curScope = new LexScope(this.curScope, this.curFn); + selfBinding = new Binding(selfName, "self", null); + this.curScope.names.set(selfName, selfBinding); + } + let info = this.enterFunction(fnNode, name); + if (selfBinding) selfBinding.fnInfo = info; + if (fnNode.body.type === "BlockStatement") this.walkFnBody(fnNode.body); + else this.walkExpr(fnNode.body); // expression-bodied arrow + this.leaveFunction(); + if (selfBinding) this.curScope = this.curScope!.parent; + this.finishAnalysis(info); + return info; + } + + // toplevel-as-EIR: analyze the whole module toplevel function. module + // bindings named in moduleSlotNames get no local binding (their + // declarations lower as slot stores, their references as slot loads); + // everything else is an ordinary toplevel local. + analyzeToplevel(fnNode: e.FunctionDeclaration, name: string, moduleSlotNames: Set): FnInfo { + this.moduleSlotNames = moduleSlotNames; + let info = this.enterFunction(fnNode, name); + info.isToplevel = true; + this.rootInfo = info; + this.walkFnBody(fnNode.body); + this.leaveFunction(); + this.finishAnalysis(info); + return info; + } + + finishAnalysis(info: FnInfo): void { + // materialize the loop envs whose bindings are captured; their + // bindings get loop-env slots (from 1; slot 0 is the parent env) + // and are excluded from function-env slot assignment below. + for (let le of this.loopEnvs) { + le.bindings = le.allBindings.filter((bd) => bd.captured); + if (le.bindings.length === 0) continue; + le.materialized = true; + le.parentSlot = 0; + let next = 1; + for (let bd of le.bindings) bd.slot = next++; + le.envSize = next; + } + assignSlots(info); + } + + // is a declaration of `name`, landing in `scope`, backed by a module + // slot (or const-literal fold) instead of a local binding? + slotBackedDecl(name: string, scope: LexScope): boolean { + return ( + this.moduleSlotNames !== null && + this.curFn === this.rootInfo && + scope.isFnTop && + this.moduleSlotNames.has(name) + ); + } + + enterFunction(fnNode: e.Function, name?: string): FnInfo { + let fname = name || (fnNode.id && fnNode.id.name) || "anon"; + // scope-qualified names aren't unique on their own (an object + // method `replace` and a toplevel function `replace` both qualify + // to `.replace`); every EIR function in a module needs a + // distinct symbol + if (this.usedFnNames.has(fname)) { + let i = 2; + while (this.usedFnNames.has(fname + "~" + i)) i++; + fname = fname + "~" + i; + } + this.usedFnNames.add(fname); + let info = new FnInfo(fnNode, fname, this.curFn); + this.fnInfos.set(fnNode, info); + + // the innermost loop env active at this definition site (in the + // DEFINING function): the closure's incoming env is that loop's + // per-iteration env, so env-chain walks must start there + let leTop = this.loopEnvStack[this.loopEnvStack.length - 1]; + info.creationLoopEnv = leTop && leTop.fnInfo === this.curFn ? leTop : null; + + if (fnNode.generator) + throw LowerNotSupported("generator function", fnNode.loc); + + this.savedLabelStacks.push(this.labelStack); + this.labelStack = []; + this.curFn = info; + this.curScope = new LexScope(this.curScope, info); + this.curScope.isFnTop = true; + // the rest parameter (a trailing RestElement, or fnNode.rest in + // older ASTs) is an ordinary local initialized from the trailing + // arguments in the prologue (see lower.js / rest_args) + let restId: e.Pattern | null = fnNode.rest ?? null; + let plainParams = fnNode.params; + let last = plainParams[plainParams.length - 1]; + if (last && last.type === "RestElement") { + restId = last.argument; + // (positive end index: the self-hosted runtime's slice-dense + // fast path crashes on negative indices — see runtime bug note + // in ejs-array.c / test/slice-negative1.js) + plainParams = plainParams.slice(0, plainParams.length - 1); + } + for (let p of plainParams) { + if (p.type !== "Identifier") + throw LowerNotSupported(`param pattern ${p.type}`, fnNode.loc); + let binding = this.curScope!.declare(p.name, "param"); + info.params.push(binding); + } + info.restBinding = null; + if (restId) { + if (restId.type !== "Identifier") + throw LowerNotSupported(`rest pattern ${restId.type}`, fnNode.loc); + info.restBinding = this.curScope!.declare(restId.name, "local"); + this.refs.set(restId, info.restBinding); + } + // default-parameter expressions are evaluated in the function scope + // (all params are declared, matching the sequential leftward-only + // visibility of the legacy DesugarDefaults lowering) + info.defaults = fnNode.defaults || []; + for (let d of info.defaults) { + if (d) this.walkExpr(d); + } + return info; + } + + leaveFunction(): void { + this.curScope = this.curScope!.parent; + this.curFn = this.curFn!.parent; + this.labelStack = this.savedLabelStacks.pop()!; + } + + pushLoopEnv(node: e.Node): LoopEnv { + let top = this.loopEnvStack[this.loopEnvStack.length - 1]; + let parentCandidate = top && top.fnInfo === this.curFn ? top : null; + let le = new LoopEnv(this.curFn, node, parentCandidate); + this.loopEnvs.push(le); + this.loopEnvByNode.set(node, le); + this.loopEnvStack.push(le); + return le; + } + + // the BODY env of a loop: captured let/const declared anywhere in the + // loop body (at any block depth, in the same function) get a fresh + // environment per iteration — their declarations re-execute each pass, + // so no value copies forward (unlike for-head vars). pushed around the + // body walk of every loop form. + pushLoopBodyEnv(node: e.Node): LoopEnv { + let top = this.loopEnvStack[this.loopEnvStack.length - 1]; + let parentCandidate = top && top.fnInfo === this.curFn ? top : null; + let le = new LoopEnv(this.curFn, node, parentCandidate); + this.loopEnvs.push(le); + this.bodyEnvByNode.set(node, le); + this.loopEnvStack.push(le); + return le; + } + + // a let/const declaration inside a loop body attaches to that loop's + // body env (top of stack, same function) + attachBodyLet(binding: Binding): void { + let top = this.loopEnvStack[this.loopEnvStack.length - 1]; + if (!top || top.fnInfo !== this.curFn) return; + binding.loopEnv = top; + top.allBindings.push(binding); + } + + reference(idNode: e.Identifier, isCallee = false): Binding | null { + if (idNode.name === "undefined") { + this.refs.set(idNode, null); + return null; + } + if (idNode.name === "arguments") { + // bind to the nearest non-arrow function's (synthetic) + // arguments object, created in its prologue + let f = this.curFn; + while (f && f.node.type === "ArrowFunctionExpression") f = f.parent; + if (!f) throw LowerNotSupported("`arguments` outside a function", idNode.loc); + if (!f.argumentsBinding) { + f.argumentsBinding = new Binding("arguments", "local", f); + f.bindings.push(f.argumentsBinding); + f.usesArguments = true; + } + let abinding = f.argumentsBinding; + this.refs.set(idNode, abinding); + if (abinding.fnInfo !== this.curFn) { + abinding.captured = true; + let g = this.curFn; + while (g && g !== abinding.fnInfo) { + g.needsParentEnv = true; + g = g.parent; + } + } + return abinding; + } + let binding = this.curScope!.lookup(idNode.name); + this.refs.set(idNode, binding); // null = global + if (!binding) { + // %-named identifiers are compiler-synthesized: ones that + // resolve to bindings (%super, pattern temps) are fine, but an + // unresolved one is a legacy-intrinsic shape lowering doesn't + // model (e.g. `%constructSuper.apply(...)` from a spread super + // call) — never a real global. fall back, don't miscompile. + if (idNode.name[0] === "%") + throw LowerNotSupported(`unresolved %-identifier ${idNode.name}`, idNode.loc); + this.globalNames.add(idNode.name); + if (!isCallee) this.globalValueNames.add(idNode.name); + return null; + } + + if (binding.kind === "self") { + // direct recursion from the function itself stays a direct + // call. anything else (value-position uses, references from + // nested functions) is treated as a free module-scope name: + // integration resolves it through the module slot when the + // candidate is exported or promoted, and falls back otherwise. + if (binding.fnInfo !== this.curFn || !isCallee) { + this.refs.set(idNode, null); + this.globalNames.add(idNode.name); + if (!isCallee) this.globalValueNames.add(idNode.name); + return null; + } + return binding; + } + + if (binding.fnInfo !== this.curFn) { + binding.captured = true; + // every function on the chain between the reference and the + // declaration needs access to its parent's environment + let f = this.curFn; + while (f && f !== binding.fnInfo) { + f.needsParentEnv = true; + f = f.parent; + } + } + return binding; + } + + // --- statements --------------------------------------------------------------- + + walkStmt(n: e.Statement): void { + switch (n.type) { + case "BlockStatement": { + this.curScope = new LexScope(this.curScope, this.curFn); + for (const s of n.body) this.walkStmt(s); + this.curScope = this.curScope!.parent; + return; + } + case "VariableDeclaration": { + // consume the for-init loop env candidate before descending + // into initializer expressions (a nested function's own + // declarations must not attach to it) + let ple = this.pendingLoopEnv; + this.pendingLoopEnv = null; + for (let d of n.declarations) { + if (d.id.type === "ObjectPattern") { + this.declareObjectPattern(n, d, ple); + continue; + } + if (d.id.type !== "Identifier") + throw LowerNotSupported(`declaration pattern ${d.id.type}`, n.loc); + // declare BEFORE walking the init: a closure created in + // the initializer must see the binding (`let walk = + // (n) => ... walk(n) ...`), or its recursive reference + // silently resolves to a global. a direct `let x = x` + // reads the pre-initialized undefined (lower.js writes + // undefined before evaluating the init), matching the + // legacy alloca behavior. + // var declarations hoist to the function scope; only + // let/const are block-scoped. + let scope = this.curScope!; + if (n.kind === "var") { + while (!scope.isFnTop) scope = scope.parent!; + } + if (this.slotBackedDecl(d.id.name, scope)) { + // toplevel module binding: no local; the declarator + // lowers as a slot store, references via refs + if (d.init) this.walkExpr(d.init); + continue; + } + let binding = scope.declare(d.id.name, "local"); + this.refs.set(d.id, binding); + if (ple) { + binding.loopEnv = ple; + ple.allBindings.push(binding); + } else if (n.kind !== "var") { + // a let/const inside a loop body: fresh binding per + // iteration if captured + this.attachBodyLet(binding); + } + if (d.init) this.walkExpr(d.init); + } + return; + } + case "FunctionDeclaration": { + if (!n.id) throw LowerNotSupported("unnamed function declaration", n.loc); + if (!this.curScope!.isFnTop) + throw LowerNotSupported("block-level function declaration", n.loc); + if (this.slotBackedDecl(n.id.name, this.curScope!)) { + // toplevel module function: no local binding — the + // closure is stored to its slot at this statement's + // position, and every reference (self-references + // included) reads the slot + let fname = `${this.curFn!.name}.${n.id.name}`; + this.enterFunction(n, fname); + this.walkFnBody(n.body); + this.leaveFunction(); + return; + } + // the walkFnBody prescan already declared this name; + // declare() hands back the same binding. genuine + // same-scope duplicates can't survive HoistFuncDecls + // (its per-name map keeps only the last declaration). + let binding = this.curScope!.declare(n.id.name, "fn"); + this.refs.set(n.id, binding); + let name = this.curFn ? `${this.curFn.name}.${n.id.name}` : n.id.name; + this.enterFunction(n, name); + this.walkFnBody(n.body); + this.leaveFunction(); + return; + } + case "ImportDeclaration": { + // toplevel mode only: scaffolding resolves the imported + // module; binding reads route through refs. a specifier + // whose local name has no slot backing (a native module's + // named import) keeps the module on the legacy path. + if (this.moduleSlotNames === null || this.curFn !== this.rootInfo) + throw LowerNotSupported("import declaration", n.loc); + for (let spec of n.specifiers) { + let local = spec.local || spec.id; + if (!local || !this.moduleSlotNames.has(local.name)) + throw LowerNotSupported( + `import binding '${local && local.name}' has no slot`, + n.loc + ); + } + return; + } + case "ExportNamedDeclaration": { + if (this.moduleSlotNames === null || this.curFn !== this.rootInfo) + throw LowerNotSupported("export declaration", n.loc); + // re-export (`export { a as b } from "m"`): the specifier + // names are the SOURCE module's exports, not local + // references — nothing to resolve here (lowering validates + // them against the source's export table) + if (n.source) return; + if (n.declaration && !Array.isArray(n.declaration)) + return this.walkStmt(n.declaration); + if (n.specifiers && n.specifiers.length > 0) { + for (let spec of n.specifiers) this.walkExpr(spec.local); + return; + } + // `export {}` — a valid, empty statement + return; + } + case "ExportDefaultDeclaration": { + if (this.moduleSlotNames === null || this.curFn !== this.rootInfo) + throw LowerNotSupported("export default", n.loc); + if ( + n.declaration.type === "FunctionDeclaration" || + n.declaration.type === "ClassDeclaration" + ) + throw LowerNotSupported("export default declaration", n.loc); + this.walkExpr(n.declaration as e.Expression); + return; + } + case "ExportAllDeclaration": + throw LowerNotSupported("export *", n.loc); + case "ExpressionStatement": + this.walkExpr(n.expression); + return; + case "IfStatement": + this.walkExpr(n.test); + this.walkStmt(n.consequent); + if (n.alternate) this.walkStmt(n.alternate); + return; + case "WhileStatement": { + this.walkExpr(n.test); + this.pushLoopBodyEnv(n); + this.walkStmt(n.body); + this.loopEnvStack.pop(); + return; + } + case "DoWhileStatement": { + this.pushLoopBodyEnv(n); + this.walkStmt(n.body); + this.loopEnvStack.pop(); + this.walkExpr(n.test); + return; + } + case "ForStatement": { + this.curScope = new LexScope(this.curScope, this.curFn); + let le = null; + if (n.init && n.init.type === "VariableDeclaration" && n.init.kind !== "var") { + le = this.pushLoopEnv(n); + } + if (n.init) { + if (n.init.type === "VariableDeclaration") { + // the declared bindings attach to the loop env + // candidate (cleared by the declaration walk before + // it descends into initializer expressions) + this.pendingLoopEnv = le; + this.walkStmt(n.init); + this.pendingLoopEnv = null; + } else this.walkExpr(n.init); + } + if (n.test) this.walkExpr(n.test); + if (n.update) this.walkExpr(n.update); + this.pushLoopBodyEnv(n); + this.walkStmt(n.body); + this.loopEnvStack.pop(); + if (le) this.loopEnvStack.pop(); + this.curScope = this.curScope!.parent; + return; + } + case "ForInStatement": + case "ForOfStatement": { + this.curScope = new LexScope(this.curScope, this.curFn); + let le = null; + if (n.left.type === "VariableDeclaration") { + const d = n.left.declarations[0]; + if (n.left.declarations.length !== 1 || !d || d.id.type !== "Identifier" || d.init) + throw LowerNotSupported("for-of/for-in binding form", n.loc); + let scope = this.curScope!; + if (n.left.kind === "var") { + while (!scope.isFnTop) scope = scope.parent!; + } + const binding = scope.declare(d.id.name, "local"); + this.refs.set(d.id, binding); + if (n.left.kind !== "var") { + le = this.pushLoopEnv(n); + binding.loopEnv = le; + le.allBindings.push(binding); + } + } else if (n.left.type === "Identifier") { + let binding = this.reference(n.left); + if (!binding) this.globalAssignedNames.add(n.left.name); + } else { + throw LowerNotSupported(`for-of/for-in target ${n.left.type}`, n.loc); + } + this.walkExpr(n.right); + this.pushLoopBodyEnv(n); + this.walkStmt(n.body); + this.loopEnvStack.pop(); + if (le) this.loopEnvStack.pop(); + this.curScope = this.curScope!.parent; + return; + } + case "SwitchStatement": { + this.walkExpr(n.discriminant); + // all case bodies share one lexical scope + this.curScope = new LexScope(this.curScope, this.curFn); + let sawDefault = false; + for (let c of n.cases) { + if (!c.test) { + if (sawDefault) + throw LowerNotSupported("duplicate default case", n.loc); + sawDefault = true; + } else { + this.walkExpr(c.test); + } + for (let s of c.consequent) this.walkStmt(s); + } + this.curScope = this.curScope!.parent; + return; + } + case "ReturnStatement": + if (n.argument) this.walkExpr(n.argument); + return; + case "ThrowStatement": + this.walkExpr(n.argument); + return; + case "TryStatement": { + let nhandlers = n.handlers ? n.handlers.length : 0; + if (nhandlers > 1) + throw LowerNotSupported("try with multiple catch clauses", n.loc); + if (nhandlers === 0 && !n.finalizer) + throw LowerNotSupported("try without catch or finally", n.loc); + this.walkStmt(n.block); + if (nhandlers === 1) { + const handler = n.handlers[0]!; + this.curScope = new LexScope(this.curScope, this.curFn); + if (handler.param) { + if (handler.param.type !== "Identifier") + throw LowerNotSupported("catch parameter pattern", n.loc); + let binding = this.curScope!.declare(handler.param.name, "catch"); + this.refs.set(handler.param, binding); + } + this.walkStmt(handler.body); + this.curScope = this.curScope!.parent; + } + if (n.finalizer) this.walkStmt(n.finalizer); + return; + } + case "LabeledStatement": { + if (this.labelStack.some((l) => l.name === n.label.name)) + throw LowerNotSupported(`duplicate label '${n.label.name}'`, n.loc); + // a label chain ending in a loop is continue-able + let body = n.body; + while (body.type === "LabeledStatement") body = body.body; + let isLoop = + body.type === "WhileStatement" || + body.type === "DoWhileStatement" || + body.type === "ForStatement" || + body.type === "ForInStatement" || + body.type === "ForOfStatement"; + this.labelStack.push({ name: n.label.name, isLoop: isLoop }); + this.walkStmt(n.body); + this.labelStack.pop(); + return; + } + case "BreakStatement": + if (n.label) { + const labelName = n.label.name; + const l = this.labelStack.find((x) => x.name === labelName); + if (!l) throw LowerNotSupported(`break to unknown label '${labelName}'`, n.loc); + } + return; + case "ContinueStatement": + if (n.label) { + const labelName = n.label.name; + const l = this.labelStack.find((x) => x.name === labelName); + if (!l || !l.isLoop) + throw LowerNotSupported(`continue to non-loop label '${labelName}'`, n.loc); + } + return; + case "EmptyStatement": + case "DebuggerStatement": // a no-op in compiled code + return; + default: + throw LowerNotSupported(`statement type ${n.type}`, n.loc); + } + } + + // `let { a, b: c, d = dflt } = init` — shallow object patterns only. + // loopEnv is the enclosing for-init loop env candidate, if any. + declareObjectPattern(declStmt: e.VariableDeclaration, d: e.VariableDeclarator, loopEnv: LoopEnv | null): void { + let scope = this.curScope!; + if (declStmt.kind === "var") { + while (!scope.isFnTop) scope = scope.parent!; + } + for (const prop of (d.id as e.ObjectPattern).properties) { + if (prop.computed) + throw LowerNotSupported("computed key in declaration pattern", declStmt.loc); + if (prop.key.type !== "Identifier" && prop.key.type !== "Literal") + throw LowerNotSupported("declaration pattern key", declStmt.loc); + let target = prop.value as e.Pattern; + let dflt: e.Expression | null = null; + if (target.type === "AssignmentPattern") { + dflt = target.right; + target = target.left; + } + if (target.type !== "Identifier") + throw LowerNotSupported( + `nested declaration pattern ${target.type}`, + declStmt.loc + ); + let binding = scope.declare(target.name, "local"); + this.refs.set(target, binding); + if (loopEnv) { + binding.loopEnv = loopEnv; + loopEnv.allBindings.push(binding); + } + if (dflt) this.walkExpr(dflt); + } + if (d.init) this.walkExpr(d.init); + } + + // --- expressions ------------------------------------------------------------ + + walkExpr(n: e.Expression | e.SpreadElement): void { + switch (n.type) { + case "Literal": + // object-valued literals are regexes (lowerable) or + // engine-specific oddities (fall back early) + if (n.value !== null && typeof n.value === "object") { + if (typeof n.value.source !== "string") + throw LowerNotSupported(`literal ${typeof n.value}`, n.loc); + } + return; + case "Identifier": + this.reference(n); + return; + case "BinaryExpression": + case "LogicalExpression": + this.walkExpr(n.left); + this.walkExpr(n.right); + return; + case "UnaryExpression": + if (n.operator === "delete" && n.argument.type !== "MemberExpression") + throw LowerNotSupported("delete of a non-member expression", n.loc); + this.walkExpr(n.argument); + return; + case "AssignmentExpression": + // compound assignments must desugar to a binop lowering + // knows; reject others here so we fall back early (a late + // lowering failure abandons the whole file's EIR set) + if (n.operator !== "=" && !compound_assign_ops[n.operator]) + throw LowerNotSupported(`assignment operator ${n.operator}`, n.loc); + if (n.left.type === "Identifier") { + let binding = this.reference(n.left); + if (!binding) this.globalAssignedNames.add(n.left.name); + } else this.walkExpr(n.left as e.Expression); + this.walkExpr(n.right); + return; + case "UpdateExpression": + if (n.argument.type === "Identifier") { + let binding = this.reference(n.argument); + if (!binding) this.globalAssignedNames.add(n.argument.name); + } else if (n.argument.type === "MemberExpression") { + this.walkExpr(n.argument); + } else { + throw LowerNotSupported(`update of ${n.argument.type}`, n.loc); + } + return; + case "TemplateLiteral": + for (let e of n.expressions) this.walkExpr(e); + return; + case "TaggedTemplateExpression": + if (n.tag.type === "Identifier") this.reference(n.tag, true); + else this.walkExpr(n.tag); + for (let e of n.quasi.expressions) this.walkExpr(e); + return; + case "CallExpression": + // %-intrinsic calls (from the pre-EIR desugar passes): + // the callee is a lowering directive, not a reference. + // only whitelisted intrinsics lower; reject others early. + if (n.callee.type === "Identifier" && n.callee.name[0] === "%") { + if (!eir_intrinsics[n.callee.name]) + throw LowerNotSupported(`intrinsic ${n.callee.name}`, n.loc); + for (let a of n.arguments) this.walkExpr(a); + return; + } + if (n.callee.type === "Identifier") this.reference(n.callee, true); + else this.walkExpr(n.callee); + for (let a of n.arguments) this.walkExpr(a); + return; + case "NewExpression": + this.walkExpr(n.callee); + for (let a of n.arguments) this.walkExpr(a); + return; + case "MemberExpression": + this.walkExpr(n.object); + if (n.computed) this.walkExpr(n.property); + return; + case "ConditionalExpression": + this.walkExpr(n.test); + this.walkExpr(n.consequent); + this.walkExpr(n.alternate); + return; + case "FunctionExpression": { + let name = (n.id && n.id.name) || `anon${this.anon_gen++}`; + this.enterFunction(n, this.curFn ? `${this.curFn.name}.${name}` : name); + this.walkFnBody(n.body); + this.leaveFunction(); + return; + } + case "ArrowFunctionExpression": { + // arrows lower as ordinary closures; lexical `this` reads + // resolve to the owner function's captured this binding + // (see the ThisExpression case below) + let name = `arrow${this.anon_gen++}`; + this.enterFunction(n, this.curFn ? `${this.curFn.name}.${name}` : name); + if (n.body.type === "BlockStatement") this.walkFnBody(n.body); + else this.walkExpr(n.body); + this.leaveFunction(); + return; + } + case "ThisExpression": { + // an arrow's `this` is lexical: capture the nearest + // non-arrow ancestor's this in its environment (the same + // shape as the `arguments` machinery above) + let f = this.curFn; + while (f && f.node.type === "ArrowFunctionExpression") f = f.parent; + // a candidate whose root IS an arrow has no owner here; + // its lexical `this` is the module toplevel's — fall back + if (!f) throw LowerNotSupported("lexical `this` in a toplevel arrow", n.loc); + if (f !== this.curFn) { + if (!f.thisBinding) { + f.thisBinding = new Binding("%this", "this", f); + f.bindings.push(f.thisBinding); + } + f.thisBinding.captured = true; + this.refs.set(n, f.thisBinding); + let g = this.curFn; + while (g && g !== f) { + g.needsParentEnv = true; + g = g.parent; + } + } + return; + } + case "SequenceExpression": + for (let e of n.expressions) this.walkExpr(e); + return; + case "ArrayExpression": + for (let e of n.elements) if (e) this.walkExpr(e); + return; + case "ObjectExpression": + for (const p of n.properties) { + if (p.computed) this.walkExpr(p.key); + this.walkExpr(p.value as e.Expression); + } + return; + default: + throw LowerNotSupported(`expression type ${n.type}`, n.loc); + } + } +} + +// assign env slots for `info` and every function below it +function assignSlots(info: FnInfo): void { + let next = 0; + // a parent pointer is only needed in the env if this function actually + // allocates one; if it doesn't, its incoming env already *is* the parent. + // captured bindings living in a per-iteration loop env got their slots + // there (analyzeFunction) and don't occupy function-env slots. + let captured = info.bindings.filter( + (bd) => bd.captured && !(bd.loopEnv && bd.loopEnv.materialized) + ); + let wantsEnv = captured.length > 0; + if (wantsEnv && info.needsParentEnv && info.parent !== null) { + info.parentSlot = next++; + } + for (let bd of captured) bd.slot = next++; + info.envSize = next; + + for (let child of info.children) assignSlots(child); +} diff --git a/lib/eir/sink-construct.ts b/lib/eir/sink-construct.ts new file mode 100644 index 00000000..78d06992 --- /dev/null +++ b/lib/eir/sink-construct.ts @@ -0,0 +1,623 @@ +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: + */ + +// Constructor-result sinking (docs/sinking-plan.md). +// +// A `new Point(x, y)` of a module-local, fence-passing constructor +// allocates an object whose fields are exactly the arguments — but the +// body's `this.x = x` stores are [[Set]] semantics, so deleting them +// statically is unsound: an accessor (or non-writable data property) +// later installed on the prototype chain must intercept every +// subsequent construction. The runtime's accessor epoch +// (_ejs_accessor_epoch, ejs-object.h) turns that global hazard into one +// load: while the epoch is still zero, NO user code has installed +// anything interception-capable anywhere, so the construct is +// observably equivalent to a fresh shaped literal of its arguments. +// +// A qualifying construct site is rewritten into an epoch-guarded +// diamond: +// +// %e = epoch_check +// cond_br %e -> ^virtual, ^slow +// ^virtual: a CLONE of the construct's use region, with the construct +// replaced by `make_object_shaped(args)` — non-escaping by the +// screens below, so the existing shaped-literal sinking drains +// the allocation, guards, and reads to pure data flow; +// ^slow: the ORIGINAL region, construct and real reads intact — +// interception semantics preserved from the first bumped epoch on. +// +// The screens are all fail-closed, and jointly guarantee the virtual +// clone's allocation always drains (all-or-nothing: a virtual arm that +// kept the allocation would carry the wrong prototype): +// +// - the callee resolves through a promoted (module-private) "%self" +// slot with a single closure store, and EVERY load of that slot is +// used only as a call/construct callee — which also proves the +// ctor's `.prototype` is never read or replaced (a swapped +// prototype could interpose an exotic object the epoch never sees); +// - the ctor's lowered body is exactly the born-with-shape fill +// diamond plus `return undefined`, its fill operands exactly the +// formals in order; the construct passes exactly that many args +// (a missing argument would change the runtime-derived shape); +// - the result's uses classify like the shaped-literal sink's, and a +// fold simulation (same guard-resolution rule as sinkShapedAlloc) +// proves every use either folds or sits in an arm the folded guards +// unreach; +// - the use region is a single-entry single-exit acyclic subgraph of +// plain br/cond_br blocks, so it can be duplicated wholesale. +// +// -fno-ctor-sink bisects this pass alone. + +import { Block, Func, Inst, Module, ShapeField } from "./ir"; +import { Effect, opInfo } from "./ops"; +import { computeRPO, computeDominators, dominates } from "./verifier"; +import { passes } from "../pass-config"; + +// region size cap: a use region bigger than this is not a constructor +// kernel, and cloning it would bloat code for a marginal win +const REGION_BLOCK_CAP = 24; + +interface CtorMatch { + fn: Func; + shape: string; + fields: readonly ShapeField[]; +} + +// does the lowered function body consist of exactly the fenced +// constructor prefix — has_shape(this,"") diamond around a +// fill_object_shaped of the formals — and `return undefined`? +function matchShapedCtor(m: Module, fn: Func): CtorMatch | null { + if (fn.blocks.length !== 4 || fn.sig) return null; + const entry = fn.entry!; + if (entry.params.length < 3) return null; // [%env, %this, formals...] + if (entry.insts.length !== 2) return null; + const thisParam = entry.params[1]!; + + const guard = entry.insts[0]!; + const cbr = entry.insts[1]!; + if (guard.op !== "has_shape" || guard.imms["shape"] !== "") return null; + if (guard.operands[0] !== thisParam) return null; + if (cbr.op !== "cond_br" || cbr.operands[0] !== guard) return null; + + const fast = cbr.targets![0]!.block; + const slow = cbr.targets![1]!.block; + if (fast.params.length > 0 || slow.params.length > 0) return null; + + // fast arm: exactly the fill + br + if (fast.insts.length !== 2) return null; + const fill = fast.insts[0]!; + const fastBr = fast.insts[1]!; + if (fill.op !== "fill_object_shaped" || fill.targets) return null; + if (fastBr.op !== "br" || fastBr.targets![0]!.args.length > 0) return null; + const join = fastBr.targets![0]!.block; + if (join.params.length > 0) return null; + + const shape = fill.imms["shape"] as string; + const fields = m.shapes.get(shape); + if (!fields) return null; + const n = fields.length; + if (entry.params.length !== 2 + n) return null; + if (fill.operands.length !== 1 + n) return null; + if (fill.operands[0] !== thisParam) return null; + for (let i = 0; i < n; i++) if (fill.operands[i + 1] !== entry.params[i + 2]) return null; + + // slow arm: the sequential twin stores, one per field, then br join + if (slow.insts.length !== n + 1) return null; + for (let i = 0; i < n; i++) { + const s = slow.insts[i]!; + if (s.op !== "set_prop_atom" || s.targets) return null; + if (s.operands[0] !== thisParam || s.operands[1] !== entry.params[i + 2]) return null; + if (s.imms["atom"] !== fields[i]!.name) return null; + } + const slowBr = slow.insts[n]!; + if (slowBr.op !== "br" || slowBr.targets![0]!.block !== join) return null; + if (slowBr.targets![0]!.args.length > 0) return null; + + // join: return undefined, nothing else + if (join.insts.length !== 2) return null; + const undef = join.insts[0]!; + const ret = join.insts[1]!; + if (undef.op !== "const" || undef.imms["kind"] !== "undefined") return null; + if (ret.op !== "return" || ret.operands[0] !== undef) return null; + + return { fn, shape, fields }; +} + +// module-wide uses of every value, as (user, operandIndex) with -1 for +// branch-edge arguments — the specialization pass's shape +interface Use { + fn: Func; + user: Inst; + operandIndex: number; +} + +function usesInModule(m: Module): Map { + const uses = new Map(); + const add = (v: Inst, u: Use) => { + let list = uses.get(v); + if (!list) uses.set(v, (list = [])); + list.push(u); + }; + for (const fn of m.functions) { + fn.forEachInst((inst) => { + inst.operands.forEach((o, i) => add(o, { fn, user: inst, operandIndex: i })); + if (inst.targets) + for (const t of inst.targets) + for (const a of t.args) if (a) add(a, { fn, user: inst, operandIndex: -1 }); + }); + } + return uses; +} + +// the shaped-literal sink's repr-provability rule: folding a shape +// guard TRUE exposes raw f64 slot loads, so every f64 field's operand +// must provably be a number +function provablyNumber(v: Inst): boolean { + return v.op === "box_f64" || (v.op === "const" && v.imms["kind"] === "number"); +} + +// a resolved constructor slot: the closure store and the matched ctor +interface SlotCtor { + match: CtorMatch; + store: Inst; + storeFn: Func; + prefixSafe: boolean; +} + +// is `a` before `b` under the dominator tree of their shared function? +function comesBefore(idom: Map, a: Inst, b: Inst): boolean { + const ba = a.block!; + const bb = b.block!; + if (ba === bb) return ba.insts.indexOf(a) < bb.insts.indexOf(b); + return dominates(idom, ba, bb); +} + +interface Candidate { + fn: Func; + construct: Inst; + match: CtorMatch; +} + +interface Region { + blocks: Set; + exit: Block; +} + +// the single-entry single-exit acyclic region rooted at `entry` that +// contains every block of `useBlocks`. null when no such region exists +// (multiple exits, outside predecessors, cycles, non-branch +// terminators, catch blocks, or over the cap). +function computeRegion(entry: Block, useBlocks: Set): Region | null { + // predecessor closure from the uses up to the entry + const blocks = new Set([entry, ...useBlocks]); + const wl: Block[] = [...useBlocks]; + while (wl.length > 0) { + const b = wl.pop()!; + if (b === entry) continue; + for (const p of b.preds()) { + if (!blocks.has(p)) { + blocks.add(p); + wl.push(p); + if (blocks.size > REGION_BLOCK_CAP) return null; + } + } + } + // structural screens + the unique exit + let exit: Block | null = null; + for (const b of blocks) { + if (b.isCatch) return null; + const t = b.terminator; + if (!t || (t.op !== "br" && t.op !== "cond_br")) return null; + for (const inst of b.insts) if (inst !== t && inst.targets) return null; + if (b !== entry) { + for (const p of b.preds()) if (!blocks.has(p)) return null; + if (b.params.some((p) => p.isException)) return null; + } + for (const s of b.succs()) { + if (blocks.has(s)) continue; + if (exit && exit !== s) return null; + exit = s; + } + } + if (!exit) return null; + // acyclic + entry-reaches-all, by DFS with an on-stack set + const state = new Map(); // 1 = on stack, 2 = done + const visit = (b: Block): boolean => { + state.set(b, 1); + for (const s of b.succs()) { + if (!blocks.has(s)) continue; + const st = state.get(s); + if (st === 1) return false; // back edge: cycle + if (st === undefined && !visit(s)) return false; + } + state.set(b, 2); + return true; + }; + if (!visit(entry)) return null; + for (const b of blocks) if (state.get(b) !== 2) return null; // unreachable from entry + return { blocks, exit }; +} + +// simulate the shaped-literal sink's guard folding over the region: +// from `entry`, a cond_br whose condition is a sole-use has_shape guard +// on `result` takes only its statically resolved edge; everything else +// takes all in-region edges. Returns the reachable block set. +function foldReachable( + region: Region, + entry: Block, + result: Inst, + shape: string, + reprsProven: boolean, + guardOf: Map // cond_br -> its foldable has_shape guard +): Set { + const reach = new Set([entry]); + const wl: Block[] = [entry]; + while (wl.length > 0) { + const b = wl.pop()!; + const t = b.terminator!; + let succs: Block[]; + const guard = t.op === "cond_br" ? guardOf.get(t) : undefined; + if (guard && guard.operands[0] === result) { + const takeTrue = guard.imms["shape"] === shape && reprsProven; + succs = [t.targets![takeTrue ? 0 : 1]!.block]; + } else { + succs = b.succs(); + } + for (const s of succs) { + if (!region.blocks.has(s) || reach.has(s)) continue; + reach.add(s); + wl.push(s); + } + } + return reach; +} + +// rewrite every qualifying construct site in the module. Returns the +// number of sites rewritten; the caller re-runs the optimizer so the +// shaped-literal sink can drain the planted virtual allocations. +export function sinkConstructResults( + m: Module, + promotedSlots: Set, + toplevelName: string | null +): number { + if (!passes().ctorSink) return 0; + if (m.shapes.size === 0) return 0; + + const toplevelFn = toplevelName + ? m.functions.find((f) => f.name === toplevelName) || null + : null; + + // %self slot traffic, module-wide + const selfStores = new Map(); + const selfLoads = new Map(); + const storeFns = new Map(); + for (const fn of m.functions) { + fn.forEachInst((inst) => { + if (inst.imms["module"] !== "%self") return; + const slot = inst.imms["slot"] as number; + if (inst.op === "module_slot_store") { + let l = selfStores.get(slot); + if (!l) selfStores.set(slot, (l = [])); + l.push(inst); + storeFns.set(inst, fn); + } else if (inst.op === "module_slot_load") { + let l = selfLoads.get(slot); + if (!l) selfLoads.set(slot, (l = [])); + l.push(inst); + } + }); + } + if (selfStores.size === 0) return 0; + + const uses = usesInModule(m); + const calleeUse = (u: Use): boolean => + u.operandIndex === 0 && + ((u.user.op === "call" && !u.user.imms["direct"]) || u.user.op === "construct"); + + // resolve each promoted slot that provably always holds one + // fence-passing constructor closure whose loads are all callees + const slotCtors = new Map(); + for (const [slot, stores] of selfStores) { + if (!promotedSlots.has(slot)) continue; + if (stores.length !== 1) continue; + const store = stores[0]!; + const closure = store.operands[0]!; + if (closure.op !== "make_closure") continue; + const ctorFn = m.functions.find((f) => f.name === closure.imms["fn"]); + if (!ctorFn) continue; + const match = matchShapedCtor(m, ctorFn); + if (!match) continue; + // every load only a callee; every OTHER use of the closure value + // is just the store itself (the specialization discipline — + // anything else could reach `.prototype`) + let ok = true; + for (const u of uses.get(closure) || []) { + if (u.user === store && u.operandIndex === 0) continue; + if (u.operandIndex === -1 || !calleeUse(u)) { + ok = false; + break; + } + } + if (ok) + for (const load of selfLoads.get(slot) || []) { + for (const lu of uses.get(load) || []) { + if (lu.operandIndex === -1 || !calleeUse(lu)) { + ok = false; + break; + } + } + if (!ok) break; + } + if (!ok) continue; + + const storeFn = storeFns.get(store)!; + // cross-function resolution needs the store to precede all user + // code: toplevel entry block, nothing CALL-shaped before it. + // (The specialization pass's rule; the loads themselves are + // pure so only calls could observe the slot in between.) + let prefixSafe = false; + if (toplevelFn && storeFn === toplevelFn && store.block === toplevelFn.entry) { + prefixSafe = true; + for (const inst of toplevelFn.entry!.insts) { + if (inst === store) break; + if ((opInfo(inst.op).effects & Effect.CALL) !== 0) { + prefixSafe = false; + break; + } + } + } + slotCtors.set(slot, { match, store, storeFn, prefixSafe }); + } + if (slotCtors.size === 0) return 0; + + // enumerate qualifying construct sites + const candidates: Candidate[] = []; + const idoms = new Map>(); + const idomOf = (fn: Func): Map => { + let d = idoms.get(fn); + if (!d) { + const { rpo } = computeRPO(fn); + idoms.set(fn, (d = computeDominators(fn, rpo))); + } + return d; + }; + for (const fn of m.functions) { + fn.forEachInst((inst) => { + if (inst.op !== "construct" || (inst.targets && inst.targets.length > 0)) return; + const callee = inst.operands[0]!; + if (callee.op !== "module_slot_load" || callee.imms["module"] !== "%self") return; + const sc = slotCtors.get(callee.imms["slot"] as number); + if (!sc) return; + // the load must provably observe the (single) store + const orderOk = sc.prefixSafe + ? !( + callee.block === sc.store.block && + sc.store.block!.insts.indexOf(callee) < + sc.store.block!.insts.indexOf(sc.store) + ) + : fn === sc.storeFn && comesBefore(idomOf(fn), sc.store, callee); + if (!orderOk) return; + if (inst.operands.length - 1 !== sc.match.fields.length) return; + candidates.push({ fn, construct: inst, match: sc.match }); + }); + } + + // a successful sink splits/clones blocks, so the module-wide use map + // goes stale — rebuild it before judging the next site + let sunk = 0; + let freshUses = uses; + for (const c of candidates) { + if (sinkOneSite(c, freshUses)) { + sunk++; + freshUses = usesInModule(m); + } + } + return sunk; +} + +function sinkOneSite(c: Candidate, uses: Map): boolean { + const { fn, construct, match } = c; + const result = construct; + const shape = match.shape; + const fields = match.fields; + const args = construct.operands.slice(1); + + // classify the result's uses: shape guards whose sole consumer is + // their block's cond_br, slot loads, own-field atom reads. Anything + // else declines the site. + const resultUses = uses.get(result) || []; + const guardOf = new Map(); // cond_br -> guard + const useBlocks = new Set(); + const guards: Inst[] = []; + const slotReads: Inst[] = []; + const atomReads: Inst[] = []; + for (const u of resultUses) { + const { user, operandIndex } = u; + if (operandIndex === -1 || user.block === null) return false; + if (user.op === "has_shape" && operandIndex === 0) { + const gu = uses.get(user) || []; + const cbr = user.block.terminator; + if ( + gu.length !== 1 || + gu[0]!.user !== cbr || + gu[0]!.operandIndex !== 0 || + !cbr || + cbr.op !== "cond_br" + ) + return false; + guardOf.set(cbr, user); + guards.push(user); + } else if (user.op === "slot_load" && operandIndex === 0) { + slotReads.push(user); + } else if (user.op === "get_prop_atom" && operandIndex === 0) { + atomReads.push(user); + } else { + return false; + } + useBlocks.add(user.block); + } + if (useBlocks.size === 0) return false; // nothing to virtualize + + const region = computeRegion(construct.block!, useBlocks); + if (!region) return false; + + // fold simulation: every use must fold (guards resolve, reads fold + // to operands) or sit in an arm the folded guards make unreachable — + // the guarantee that the virtual clone's allocation fully drains + const reprsProven = fields.every( + (f, i) => f.repr !== "f64" || provablyNumber(args[i]!) + ); + const reach = foldReachable(region, construct.block!, result, shape, reprsProven, guardOf); + const fieldIndex = (name: string): number => { + for (let i = 0; i < fields.length; i++) if (fields[i]!.name === name) return i; + return -1; + }; + // (guards need no reachability screen: only sole-use cond_br guards + // got this far, and those always resolve statically) + for (const r of slotReads) { + if (!reach.has(r.block!)) continue; + if (r.targets) return false; + if (r.imms["shape"] !== shape) return false; + const k = r.imms["slot"] as number; + if (k < 0 || k >= fields.length) return false; + if (r.imms["repr"] === "f64" && !provablyNumber(args[k]!)) return false; + } + for (const r of atomReads) { + if (!reach.has(r.block!)) continue; + if (r.targets) return false; + if (fieldIndex(r.imms["atom"] as string) < 0) return false; // prototype read + } + + // live-outs: values defined in the region (below the construct) and + // used at-or-after the exit need a join param each + const regionDefs = new Set(); + const b0 = construct.block!; + const splitAt = b0.insts.indexOf(construct); + for (const b of region.blocks) { + for (const p of b.params) if (b !== b0) regionDefs.add(p); + const from = b === b0 ? splitAt : 0; + for (let i = from; i < b.insts.length; i++) regionDefs.add(b.insts[i]!); + } + const liveOuts: Inst[] = []; + for (const v of regionDefs) { + for (const u of uses.get(v) || []) { + if (u.user.block && !region.blocks.has(u.user.block)) { + // an i1 can never cross a block boundary, raw or joined + if (v.type === "i1") return false; + liveOuts.push(v); + break; + } + } + } + if (liveOuts.length > 0) { + // the exit's params can only absorb them if every exit + // predecessor is ours + for (const p of region.exit.preds()) if (!region.blocks.has(p)) return false; + } + + // ---- rewrite ------------------------------------------------- + // 1. split the construct's block: the head keeps everything before + // the construct and gains the epoch diamond; the tail (construct + // included) becomes the slow arm's entry. Successor predEdges + // reference terminator INSTRUCTIONS, so moving the instructions + // keeps the edge bookkeeping consistent. + const slowEntry = new Block(fn, "ctor_slow"); + slowEntry.sealed = true; + fn.blocks.push(slowEntry); + slowEntry.insts = b0.insts.splice(splitAt); + for (const inst of slowEntry.insts) inst.block = slowEntry; + + const cloneOf = new Map(); + const regionBlocks: Block[] = [slowEntry]; + for (const b of region.blocks) if (b !== b0) regionBlocks.push(b); + + // 2. clone the region; the construct becomes a shaped literal of + // the arguments + const valueMap = new Map(); + for (const b of regionBlocks) { + const cb = new Block(fn, "ctor_virtual"); + cb.sealed = true; + fn.blocks.push(cb); + cloneOf.set(b, cb); + for (const p of b.params) { + const cp = cb.addParam(p.nameHint); + cp.type = p.type; + cp.rawJoin = p.rawJoin; + valueMap.set(p, cp); + } + } + const mapVal = (v: Inst): Inst => valueMap.get(v) || v; + // live-out join params, appended to the exit's existing ones. The + // slow arm's exit edges pass the original values, the clone's edges + // the cloned ones; adding them BEFORE the clone's terminators exist + // extends only the original edges with the null slots filled here. + const exitParams = new Map(); + for (const v of liveOuts) { + const p = region.exit.addParam("ctor_sink"); + p.type = v.type; + p.rawJoin = v.type === "f64"; + exitParams.set(v, p); + for (const e of region.exit.predEdges) { + const t = e.inst.targets![e.targetIndex]!; + if (t.args[t.args.length - 1] === null) t.args[t.args.length - 1] = v; + } + } + for (const b of regionBlocks) { + const cb = cloneOf.get(b)!; + for (const inst of b.insts) { + let clone: Inst; + if (inst === construct) { + clone = new Inst(fn, "make_object_shaped", args.map(mapVal), { shape: shape }); + } else { + clone = new Inst(fn, inst.op, inst.operands.map(mapVal), { ...inst.imms }); + clone.type = inst.type; + } + clone.block = cb; + cb.insts.push(clone); + valueMap.set(inst, clone); + if (inst.targets) { + for (const t of inst.targets) { + // exit edges were already extended with the live-out + // args above, so mapping the originals covers them + const target = cloneOf.get(t.block) || t.block; + clone.addTarget(target, t.args.map((a) => (a ? mapVal(a) : a)), t.kind); + } + } + } + } + + // 3. the epoch diamond closes the head + const epoch = new Inst(fn, "epoch_check", [], {}); + epoch.block = b0; + b0.insts.push(epoch); + const cbr = new Inst(fn, "cond_br", [epoch], {}); + cbr.block = b0; + b0.insts.push(cbr); + cbr.addTarget(cloneOf.get(slowEntry)!, []); + cbr.addTarget(slowEntry, []); + + // 4. everything at or beyond the exit sees the live-outs through + // the new join params + if (liveOuts.length > 0) { + const cloneSet = new Set(cloneOf.values()); + fn.forEachInst((inst) => { + const b = inst.block; + if (!b || region.blocks.has(b) || b === slowEntry || cloneSet.has(b)) return; + for (let i = 0; i < inst.operands.length; i++) { + const p = exitParams.get(inst.operands[i]!); + if (p) inst.operands[i] = p; + } + if (inst.targets) + for (const t of inst.targets) + for (let i = 0; i < t.args.length; i++) { + const a = t.args[i]; + if (a) { + const p = exitParams.get(a); + if (p) t.args[i] = p; + } + } + }); + } + return true; +} diff --git a/lib/eir/sink-flow.ts b/lib/eir/sink-flow.ts new file mode 100644 index 00000000..4a518ba0 --- /dev/null +++ b/lib/eir/sink-flow.ts @@ -0,0 +1,516 @@ +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: + */ + +// Flow-sensitive allocation sinking + partial-escape materialization +// (docs/sinking-plan.md, sinking-P3). +// +// Extends the flow-insensitive sinks in optimize.ts to object +// candidates WITH field writes, and to candidates with exactly one +// escaping use. Two structural facts carry the design: +// +// - propSet's write diamonds are twins: both arms store the same +// source value, so the after-join tracked value of a written field +// is just that value — field phis are needed only at REAL control +// joins (if/else arms writing different values, loop headers). +// - Folding a shape guard FALSE is unconditionally sound (the +// sinking-P1 twin argument), independent of writes. A written +// candidate folds every guard false and resolves everything +// through the generic arms; the memory ops then vanish entirely, +// and the post-fixpoint rawJoin/guard-region passes recover raw +// f64 flow on the values. (Folding TRUE under writes would need +// repr-invariance reasoning — a set_prop_atom storing a non-number +// into an f64 field repr-transitions the runtime shape.) +// +// The pass is all-or-nothing per candidate, and PLANS before it +// mutates: guard folding, though sound, routes the object generic, so +// a fold-then-decline would pessimize a surviving allocation. Only +// when every screen passes does the rewrite run: +// +// 1. fold each guard's branch to its false edge, sweep the dead arms; +// 2. Braun-rename each field over the folded CFG (boxed block params +// minted at joins, trivial ones removed) and fold every read to +// its reaching value; +// 3. (partial escape) materialize a fresh literal of the reaching +// field values immediately before the single escape instruction +// and substitute it there — the runtime re-derives the true shape +// from the actual values, so tracked-write repr drift is +// immaterial; +// 4. delete the writes and the allocation. +// +// Fail-closed screens, with the reasons recorded in sinking-plan.md: +// own-key target-less writes only (a key-adding [[Set]] walks the +// prototype chain); no computed reads; no catch blocks in the rename +// region (unwind edges never carry tracked values); no use reachable +// from the escape (a later read would miss mutations through the +// alias); the escape executes at most once per allocation (a forward +// walk from the escape that finds any use — itself included — without +// first re-entering the allocation's block declines); the escape +// instruction plays no second role (a `o.self = o` write-escape +// declines). +// +// -fno-flow-sink bisects this pass alone. + +import { Block, Func, Inst, Module, replaceAllUses } from "./ir"; +// type-only imports: a value import would make optimize <-> sink-flow a +// runtime module cycle +import type { OptStats, Use, UseMap } from "./optimize"; +import { condBrToBr, sweepUnreachableBlocks } from "./optimize-guards"; + +// the driver's per-round use map, indexed by inst.id (see the +// allocation-churn note on scanRound in optimize.ts — this pass runs +// last in the round and does no scans of its own) +const NO_USES: Use[] = []; + +function usesOf(map: UseMap, v: Inst): Use[] { + return map[v.id] || NO_USES; +} + +// the candidate's field universe: names in literal order. Shaped +// allocations key off the module shape table; unshaped ones off +// imms.keys (duplicate keys collapse to the LAST position's value, the +// ownObjectValue rule). +interface FieldInfo { + names: string[]; + // field name -> operand index holding its initial value + initial: Map; +} + +function fieldsOf(m: Module | undefined, alloc: Inst): FieldInfo | null { + if (alloc.op === "make_object_shaped") { + if (!m) return null; + const fields = m.shapes.get(alloc.imms.shape as string); + if (!fields || fields.length !== alloc.operands.length) return null; + const initial = new Map(); + const names: string[] = []; + fields.forEach((f, i) => { + names.push(f.name); + initial.set(f.name, i); + }); + return { names, initial }; + } + // make_object + const keys = alloc.imms.keys as readonly string[]; + const initial = new Map(); + const names: string[] = []; + keys.forEach((k, i) => { + if (!initial.has(k)) names.push(k); + initial.set(k, i); // last definition wins + }); + // a __proto__ key is a prototype set, not a field; not ours + if (initial.has("__proto__")) return null; + return { names, initial }; +} + +// rename-region size cap (the ctor-sink REGION_BLOCK_CAP precedent, +// but for a COST model rather than a cloning one): sinking spreads the +// object's field values across the whole alloc-to-use region as live +// SSA values, so a large region trades one heap object for many +// long-lived gc-frame slots — measured on the stage1 self-compile, +// where flow-sinking esprima's scanPunctuator token literal (a +// function-spanning region) doubled the conservative pin-scan cost of +// every minor GC during parses and nearly doubled compile wall time. +// Small regions (loop accumulators, builder tails) keep the win. +const FLOW_REGION_CAP = 32; + +// the classified plan for one candidate; built without mutating +interface Plan { + guards: Inst[]; // has_shape, sole consumer its block's cond_br + reads: Inst[]; // reachable own-key get_prop_atom, target-less + writes: Inst[]; // reachable own-key set_prop_atom, target-less + escape: Inst | null; // the single escape instruction, if any + reachable: Set; // under folded guard branches +} + +// successors under the fold plan: a cond_br whose condition is one of +// the candidate's guards takes only its false edge +function foldedSuccs(b: Block, guardSet: Set): Block[] { + const t = b.terminator; + if (!t || !t.targets) return []; + if (t.op === "cond_br" && guardSet.has(t.operands[0]!)) return [t.targets[1]!.block]; + return t.targets.map((tg) => tg.block); +} + +function computeFoldedReachable(fn: Func, guardSet: Set): Set { + const reach = new Set([fn.entry!]); + const wl: Block[] = [fn.entry!]; + while (wl.length > 0) { + const b = wl.pop()!; + for (const s of foldedSuccs(b, guardSet)) { + if (!reach.has(s)) { + reach.add(s); + wl.push(s); + } + } + } + return reach; +} + +// classify + screen one candidate; null = decline (nothing mutated) +function planOne(useMap: UseMap, fields: FieldInfo, alloc: Inst, uses: Use[]): Plan | null { + const guards: Inst[] = []; + const guardSet = new Set(); + const reads: Inst[] = []; + const writes: Inst[] = []; + const slotOps: Inst[] = []; + const escapes = new Map(); // inst -> true (dedup multi-operand escapes) + const roles = new Map(); // 1=read/write, 2=escape (bitmask) + const fn = alloc.block!.fn; + + for (const use of uses) { + const { inst, index } = use; + if (inst.block === null) continue; // already removed elsewhere this round + if (index === -1) { + escapes.set(inst, true); + roles.set(inst, (roles.get(inst) ?? 0) | 2); + } else if (inst.op === "has_shape" && index === 0) { + // foldable only when its sole consumer is its block's cond_br + const guardUses = usesOf(useMap, inst); + if ( + guardUses.length === 1 && + guardUses[0]!.inst.op === "cond_br" && + guardUses[0]!.index === 0 && + guardUses[0]!.inst.block === inst.block + ) { + guards.push(inst); + guardSet.add(inst); + } else { + return null; // unfoldable guard keeps the object alive + } + } else if (inst.op === "get_prop_atom" && index === 0) { + if (inst.targets) return null; + if (!fields.initial.has(inst.imms.atom as string)) return null; // prototype read + reads.push(inst); + roles.set(inst, (roles.get(inst) ?? 0) | 1); + } else if (inst.op === "set_prop_atom" && index === 0) { + if (inst.targets) return null; + if (!fields.initial.has(inst.imms.atom as string)) return null; // key-adding write + writes.push(inst); + roles.set(inst, (roles.get(inst) ?? 0) | 1); + } else if ((inst.op === "slot_load" || inst.op === "slot_store") && index === 0) { + // these live in guarded fast arms; the fold must unreach them + slotOps.push(inst); + } else { + escapes.set(inst, true); + roles.set(inst, (roles.get(inst) ?? 0) | 2); + } + } + + // cheap pre-screen: pure-read candidates belong to the + // flow-insensitive sinks — skip the CFG walks entirely + if (writes.length === 0 && escapes.size === 0) return null; + + const reachable = computeFoldedReachable(fn, guardSet); + if (!reachable.has(alloc.block!)) return null; // dead code: not ours to judge + + // every slot op must die with its arm; a reachable one means the + // guard structure is not the lowering's (hand-built IR): decline + for (const s of slotOps) if (reachable.has(s.block!)) return null; + + const liveReads = reads.filter((r) => reachable.has(r.block!)); + const liveWrites = writes.filter((w) => reachable.has(w.block!)); + const liveEscapes = [...escapes.keys()].filter((e) => reachable.has(e.block!)); + + if (liveEscapes.length > 1) return null; + const escape = liveEscapes.length === 1 ? liveEscapes[0]! : null; + // the escape instruction must play no second role + if (escape && (roles.get(escape)! & 1) !== 0) return null; + + // this pass exists for writes and escapes; pure read candidates + // belong to the flow-insensitive sinks + if (liveWrites.length === 0 && !escape) return null; + // materializing at the escape must gain something + if (escape && liveWrites.length === 0 && liveReads.length === 0) return null; + + // no use may be reachable FROM the escape (post-escape reads would + // miss mutations through the alias; re-reaching the escape itself + // would split the object's identity). Re-entering the allocation's + // block starts a fresh activation and stops the walk. + if (escape) { + const useInsts = new Set([...guards, ...liveReads, ...liveWrites, escape]); + const eb = escape.block!; + const after = eb.insts.slice(eb.insts.indexOf(escape) + 1); + for (const i of after) if (useInsts.has(i)) return null; + const wl = foldedSuccs(eb, guardSet).filter((s) => s !== alloc.block); + const seen = new Set(wl); + while (wl.length > 0) { + const b = wl.pop()!; + if (!reachable.has(b)) continue; + for (const i of b.insts) if (useInsts.has(i)) return null; + for (const s of foldedSuccs(b, guardSet)) { + if (s === alloc.block || seen.has(s)) continue; + seen.add(s); + wl.push(s); + } + } + } + + // the rename region: reachable blocks the backward walk from the + // uses can visit, up to (and excluding past) the allocation's + // block. No catch blocks — an unwind edge can't carry a tracked + // value into a minted param. + const useBlocks = new Set(); + for (const i of [...liveReads, ...liveWrites]) useBlocks.add(i.block!); + if (escape) useBlocks.add(escape.block!); + const region = new Set(useBlocks); + const wl = [...useBlocks]; + while (wl.length > 0) { + const b = wl.pop()!; + if (b === alloc.block) continue; + for (const e of b.predEdges) { + const p = e.inst.block!; + if (!reachable.has(p) || region.has(p)) continue; + region.add(p); + wl.push(p); + } + } + for (const b of region) if (b.isCatch) return null; + if (region.size > FLOW_REGION_CAP) return null; + + return { guards, reads: liveReads, writes: liveWrites, escape, reachable }; +} + +// --- the rewrite ----------------------------------------------------------- + +// Braun-style per-field renaming over the (already folded and swept) +// CFG. Values are boxed SSA values; params minted at joins are boxed +// "any" params, verifier-legal on every edge. +class FieldRenamer { + private fn: Func; + private alloc: Inst; + private fields: FieldInfo; + // per block: candidate writes, relative order preserved + private writesIn = new Map(); + // field -> block -> value at block ENTRY (params minted here; this + // is the cycle-breaker memo, deliberately separate from the write + // scan so a block that both joins and writes resolves reads before + // its write to the entry value and reads after it to the write's) + private entryMemo = new Map>(); + // trivial-param forwarding chain. A recursion frame can capture a + // param that a NESTED cascade then removes — its replaceAllUses + // runs before the outer frame installs the stale capture — so + // every install point resolves through this map first. + private forwarded = new Map(); + + resolve(v: Inst): Inst { + for (;;) { + const n = this.forwarded.get(v); + if (!n) return v; + v = n; + } + } + + constructor(fn: Func, alloc: Inst, fields: FieldInfo, writes: Inst[]) { + this.fn = fn; + this.alloc = alloc; + this.fields = fields; + for (const w of writes) { + const b = w.block!; + let list = this.writesIn.get(b); + if (!list) this.writesIn.set(b, (list = [])); + list.push(w); + } + for (const list of this.writesIn.values()) + list.sort((a, b) => a.block!.insts.indexOf(a) - b.block!.insts.indexOf(b)); + } + + private memoFor(field: string): Map { + let m = this.entryMemo.get(field); + if (!m) this.entryMemo.set(field, (m = new Map())); + return m; + } + + // the reaching value at a program point: before insts[uptoIndex] of + // `block` (uptoIndex past the end = block exit) + valueAt(field: string, block: Block, uptoIndex: number): Inst { + const list = this.writesIn.get(block); + if (list) { + for (let i = list.length - 1; i >= 0; i--) { + const w = list[i]!; + if ((w.imms.atom as string) !== field) continue; + const wi = block.insts.indexOf(w); + if (wi >= 0 && wi < uptoIndex) return w.operands[1]!; + } + } + if (block === this.alloc.block) { + const ai = block.insts.indexOf(this.alloc); + if (ai >= 0 && ai < uptoIndex) + return this.alloc.operands[this.fields.initial.get(field)!]!; + } + return this.valueAtEntry(field, block); + } + + private valueAtEnd(field: string, block: Block): Inst { + return this.valueAt(field, block, block.insts.length); + } + + private valueAtEntry(field: string, block: Block): Inst { + const memo = this.memoFor(field); + const hit = memo.get(block); + if (hit) return this.resolve(hit); + + const preds = block.predEdges; + if (preds.length === 1) { + const v = this.resolve(this.valueAtEnd(field, preds[0]!.inst.block!)); + memo.set(block, v); + return v; + } + + // join: mint a boxed param, memoized BEFORE recursing so loop + // back-edges resolve to it. argIndexOfParam is recomputed per + // edge — a trivial-param removal during the recursion can + // renumber this block's params — and the dependent-recheck + // cascade can forward THIS param mid-fill, in which case the + // memo already holds its replacement. + const param = block.addParam("sink_" + field); + memo.set(block, param); + for (const e of preds) { + const v = this.resolve(this.valueAtEnd(field, e.inst.block!)); + if (param.removed) break; + e.inst.targets![e.targetIndex]!.args[block.argIndexOfParam(param)] = v; + } + if (param.removed) return this.resolve(memo.get(block)!); + return this.tryRemoveTrivialParam(param); + } + + // the builder's trivial-param rule: a param whose incoming + // arguments are all the same value (or itself) forwards that value. + // Dependent sink params (which may use this one as an edge + // argument) are rechecked after the forward. + private tryRemoveTrivialParam(param: Inst): Inst { + if (param.removed) return param; + const block = param.block!; + const argIdx = block.argIndexOfParam(param); + let same: Inst | null = null; + for (const e of block.predEdges) { + const arg = e.inst.targets![e.targetIndex]!.args[argIdx]; + // an unfilled slot means the param is mid-fill higher up + // the recursion — never judge it yet + if (!arg) return param; + if (arg === same || arg === param) continue; + if (same !== null) return param; + same = arg; + } + if (same === null) return param; + same = this.resolve(same); + + replaceAllUses(this.fn, param, same); + this.forwarded.set(param, same); + const dependents: Inst[] = []; + for (const m of this.entryMemo.values()) + for (const [b, v] of m.entries()) + if (v === param) { + m.set(b, same); + } else if (v.op === "blockparam" && !v.removed && v !== param) { + dependents.push(v); + } + block.removeParam(param); + for (const d of dependents) if (!d.removed) this.tryRemoveTrivialParam(d); + return same; + } +} + +function removeFromBlock(inst: Inst): void { + const b = inst.block!; + const idx = b.insts.indexOf(inst); + if (idx >= 0) b.insts.splice(idx, 1); + inst.block = null; +} + +function applyPlan( + fn: Func, + fields: FieldInfo, + alloc: Inst, + plan: Plan, + stats: OptStats +): void { + // 1. fold the guard branches false and reclaim the dead arms (this + // disconnects every slot op and the fast-arm halves of the + // read/write diamonds; predEdges stay consistent for the renamer) + for (const g of plan.guards) { + const block = g.block!; + const cbr = block.terminator; + if (!cbr || cbr.op !== "cond_br" || cbr.operands[0] !== g) continue; + condBrToBr(fn, block, 1); + stats.shape_guards_sunk++; + } + sweepUnreachableBlocks(fn); + + const renamer = new FieldRenamer(fn, alloc, fields, plan.writes); + + // 2. fold every read to its reaching value + for (const read of plan.reads) { + if (!read.block) continue; // swept + const v = renamer.resolve( + renamer.valueAt(read.imms.atom as string, read.block, read.block.insts.indexOf(read)) + ); + replaceAllUses(fn, read, v); + removeFromBlock(read); + stats.reads_folded++; + } + + // 3. materialize at the single escape, if any + if (plan.escape && plan.escape.block) { + const e = plan.escape; + const eb = e.block!; + const at = eb.insts.indexOf(e); + // resolve AFTER all valueAt calls: a later field's renaming can + // forward a param an earlier field's value captured + const values = ( + alloc.op === "make_object_shaped" + ? fields.names + : (alloc.imms.keys as readonly string[]) + ) + .map((k) => renamer.valueAt(k, eb, at)) + .map((v) => renamer.resolve(v)); + const made = + alloc.op === "make_object_shaped" + ? new Inst(fn, "make_object_shaped", values, { shape: alloc.imms.shape }) + : new Inst(fn, "make_object", values, { keys: alloc.imms.keys }); + made.block = eb; + eb.insts.splice(at, 0, made); + for (let i = 0; i < e.operands.length; i++) if (e.operands[i] === alloc) e.operands[i] = made; + if (e.targets) + for (const t of e.targets) + for (let i = 0; i < t.args.length; i++) if (t.args[i] === alloc) t.args[i] = made; + stats.allocs_materialized++; + } + + // 4. the writes and the allocation go + for (const w of plan.writes) if (w.block) removeFromBlock(w); + removeFromBlock(alloc); + stats.flow_allocs_sunk++; +} + +// try to flow-sink candidates in `fn`; at most ONE rewrite per call +// (the rewrite reshapes the CFG, so later candidates re-plan against +// fresh state on the driver's next fixpoint round). The bisect flag +// (-fno-flow-sink) is read by the driver, not here (SinkFlags note), +// and the use map + candidate lists come from the driver's single +// per-round scan — this pass MUTATES without maintaining the map, so +// it must stay the round's last consumer. Returns whether anything +// changed. +export function sinkFlowAllocations( + fn: Func, + m: Module | undefined, + stats: OptStats, + useMap: UseMap, + objCandidates: Inst[], + shapedCandidates: Inst[] +): boolean { + const tryOne = (alloc: Inst): boolean => { + if (!alloc.block) return false; + const fields = fieldsOf(m, alloc); + if (!fields) return false; + const plan = planOne(useMap, fields, alloc, usesOf(useMap, alloc)); + if (!plan) return false; + applyPlan(fn, fields, alloc, plan, stats); + return true; + }; + for (const alloc of objCandidates) { + if (alloc.op !== "make_object") continue; // make_array: not ours + if (tryOne(alloc)) return true; + } + if (m) for (const alloc of shapedCandidates) if (tryOne(alloc)) return true; + return false; +} diff --git a/lib/eir/specialize.ts b/lib/eir/specialize.ts new file mode 100644 index 00000000..f19ae01a --- /dev/null +++ b/lib/eir/specialize.ts @@ -0,0 +1,735 @@ +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: + */ + +// typed calling convention / function specialization. For a function with a LOCAL CLOSED WORLD — its +// closure value never escapes and every call site is enumerated +// in-module — emit a specialized clone with an unboxed signature +// (f64 formals, f64 result), rewrite the provably-known call sites to +// direct calls that unbox at the caller, and never emit the slow paths +// in the clone at all (SpecMode lowering). +// +// The trust story crosses the guarded line ON PURPOSE: oracle +// claims become facts inside the clone and at rewritten call sites. +// What keeps that honest: +// - the differential harness (hard precondition) validates the +// oracle's abstraction against concrete execution; +// - the escape analysis here is COMPILER-side and structural (operand +// flow over lowered EIR) — it does not consult the oracle, so a +// wrong oracle can never widen the set of functions we specialize; +// a function that LOOKS closed-world but isn't is rejected by +// construction (any non-callee use of the closure value, or any +// slot flow we can't fully enumerate, kills the candidate); +// - structural post-checks on the lowered clone (env/this unused, no +// frame ops, every return actually f64) discard any clone whose body +// could not honor the signature — trust-free, independent of why. +// +// Two closure-flow shapes are recognized (v1): +// - SSA-visible: every use of the make_closure value is the callee +// of a plain call in the same function; +// - promoted-slot: the single store of the closure into a promoted +// (non-exported, module-private) "%self" slot, where every load of +// that slot is used only as a plain-call callee. Loads in the +// storing function are rewritten when the store dominates the load; +// loads elsewhere keep the generic path (still enumerated — they +// call the generic function, never the clone). +// +// Module-level EXPORTS are never trusted-specialization candidates: the +// slot-based module ABI exposes boxed ejsvals to JS and native +// consumers (non-promoted slots are readable through importer slot +// loads and accessor functions), so only promoted slots — invisible +// outside the module — qualify. +// +// --- the escape taint and the export-boundary wrapper (runtime-P2) --- +// +// The analysis maam runs covers THIS module's executions only. Any +// closure that escapes (canonically: stored in a non-promoted export +// slot) can be called by code the analysis never saw, and maam's value +// domain is constant-propagation — its claims about such a function's +// body may hold only for the argument CONSTANTS it analyzed, so even +// all-number external arguments can escape them. Two consequences: +// +// ESCAPE TAINT. `tainted` is the set of Funcs whose activations can +// observe un-analyzed values: the escaping closures themselves, +// closed under (a) the callee of any enumerated call site hosted in +// a tainted function (its arguments are tainted), and (b) any +// closure created inside a tainted function (its captured +// environment is tainted). Unknown-callee calls need no edge: a +// value only becomes callable from tainted code by flowing there, +// which classifies its function as escaping. Everything OUTSIDE the +// set runs only during module init — before any external caller can +// exist — so oracle claims about it keep their whole-program cover. +// (Residual, documented: an import CYCLE can re-enter a module +// mid-init; taint does not model that corner.) No call site HOSTED +// in a tainted function is ever rewritten to a trusted clone, and no +// ESCAPING function gets one. A tainted-but-non-escaping helper may +// still be trusted-cloned: the clone is entered only through +// rewritten sites in covered code, whose activations all run during +// init — its tainted (generic-entry) activations never reach it. +// +// THE WRAPPER. An escaping entry point still gets a typed fast +// path, but a trust-free one: an UNTRUSTED clone (guarded body — +// ordinary diamonds, assume-and-guard gate, boxed "any" result) with +// f64 formals boxed once at entry, plus a boundary wrapper spliced +// into the generic function's entry: one has_tag(number) guard per +// formal, all-pass dispatching to the clone via call_typed, any +// failure falling through to the original generic body. The entry +// box_f64 proofs let the optimizer fold the formal-rooted diamonds +// structurally, so the clone approaches trusted-clone quality +// without consuming a single oracle claim as fact — exports keep +// full dynamic semantics, external callers included. Internal +// callers reach the same guards through the generic entry (devirt +// direct-calls it; LLVM can inline the prologue). +// -fno-export-wrapper bisects the wrapper alone. + +import { Module, Func, Inst, Block } from "./ir"; +import { Effect, opInfo } from "./ops"; +import { computeRPO, computeDominators, dominates } from "./verifier"; +import { lowerSpecializedClone } from "./lower"; +import type { ModCtx, SpecMode } from "./lower"; +import type { ScopeAnalysis, FnInfo } from "./scopes"; +import type { TypeOracle } from "./oracle"; +import type * as e from "../estree"; +import { passes } from "../pass-config"; + +export interface SpecStats { + // clones emitted + specialized: number; + // call sites rewritten to call_typed + sites: number; + // candidates whose lowered clone failed the structural post-checks + rejected: number; + // escaping entry points that received a boundary wrapper + wrapped: number; + // call sites left generic because their host is escape-tainted + fenced: number; +} + +// one use of a value: the using instruction and where the value appears +interface Use { + fn: Func; + user: Inst; + // operand index, or -1 when the value is a branch-edge argument + operandIndex: number; +} + +function usesInModule(m: Module): Map { + const uses = new Map(); + const add = (v: Inst, u: Use) => { + let list = uses.get(v); + if (!list) uses.set(v, (list = [])); + list.push(u); + }; + for (const fn of m.functions) { + fn.forEachInst((inst) => { + inst.operands.forEach((o, i) => add(o, { fn, user: inst, operandIndex: i })); + if (inst.targets) + for (const t of inst.targets) + for (const a of t.args) if (a) add(a, { fn, user: inst, operandIndex: -1 }); + }); + } + return uses; +} + +// does the oracle type this node as exactly {number}? numeric literals +// qualify directly (the oracle's mapping policy leaves literals +// unmapped), mirroring LowerFunction.operandIsNumber. +function nodeIsNumber(oracle: TypeOracle, node: e.Node): boolean { + const lit = node as { type?: string; value?: unknown }; + if (lit.type === "Literal") return typeof lit.value === "number"; + if (lit.type === "UnaryExpression") { + const u = node as e.UnaryExpression; + if ( + (u.operator === "-" || u.operator === "+") && + u.argument.type === "Literal" && + typeof (u.argument as e.Literal).value === "number" + ) + return true; + } + const t = oracle.typeOfNode(node); + return t.tags !== "top" && t.tags.size === 1 && t.tags.has("number"); +} + +// could this formal be a number at runtime? The wrapper's oracle use is +// heuristic only (guards decide) — decline only a POSITIVE non-number +// claim, where the guard chain could never pass. +function nodeMayBeNumber(oracle: TypeOracle, node: e.Node): boolean { + const t = oracle.typeOfNode(node); + return t.tags === "top" || t.tags.has("number"); +} + +// the ReturnStatement nodes of fn's own body (nested functions excluded) +function ownReturns(fnNode: e.Function): e.ReturnStatement[] { + const out: e.ReturnStatement[] = []; + const walk = (n: unknown): void => { + if (!n || typeof n !== "object") return; + if (Array.isArray(n)) { + for (const x of n) walk(x); + return; + } + const node = n as { type?: string } & Record; + if (typeof node.type !== "string") return; + if ( + node.type === "FunctionDeclaration" || + node.type === "FunctionExpression" || + node.type === "ArrowFunctionExpression" + ) + return; + if (node.type === "ReturnStatement") out.push(node as unknown as e.ReturnStatement); + for (const k of Object.keys(node)) { + if (k === "loc" || k === "range") continue; + walk(node[k]); + } + }; + walk(fnNode.body); + return out; +} + +// ops a specialized clone must not contain (they need the generic +// calling convention's argc/args/newTarget/this machinery) +const CLONE_FRAME_OPS = new Set([ + "args_obj", + "rest_args", + "arg_len", + "new_target", + "construct_super", + "construct_super_apply", +]); + +interface CallSite { + call: Inst; + fn: Func; + rewritable: boolean; +} + +// the structural closure-flow classification of one function +interface ClosureFlow { + // false when no make_closure for it remains (inlined/DCE'd) + referenced: boolean; + escapes: boolean; + // complete only when !escapes (the scan stops at the first escape) + sites: CallSite[]; +} + +// find the Func containing an instruction's block (blocks know their fn) +function fnOf(inst: Inst): Func { + return inst.block!.fn; +} + +function uniqueCloneName(m: Module, base: string): string { + const names = new Set(m.functions.map((f) => f.name)); + let name = base; + for (let i = 1; names.has(name); i++) name = `${base}$${i}`; + return name; +} + +// is `a` (in block ba at index ia) before `b` (in bb at ib) under dom? +function comesBefore( + idom: Map, + a: Inst, + b: Inst +): boolean { + const ba = a.block!; + const bb = b.block!; + if (ba === bb) return ba.insts.indexOf(a) < bb.insts.indexOf(b); + return dominates(idom, ba, bb); +} + +// the escape-taint fixpoint (see the file comment). Monotone across +// rounds: wrapper clones are added by the caller at minting time; the +// edges a wrapper clone contributes duplicate its generic twin's (same +// AST, same sites, same children), so membership never grows late. +function computeEscapeTaint( + m: Module, + flows: Map, + funcsByName: Map, + tainted: Set +): void { + for (const [info, flow] of flows) if (flow.escapes) tainted.add(info.fn!); + for (;;) { + let grew = false; + // (a) a tainted host's call arguments are tainted values + for (const [info, flow] of flows) { + if (tainted.has(info.fn!)) continue; + if (flow.sites.some((s) => tainted.has(s.fn))) { + tainted.add(info.fn!); + grew = true; + } + } + // (b) a closure created in a tainted host captures tainted state + for (const fn of m.functions) { + if (!tainted.has(fn)) continue; + fn.forEachInst((inst) => { + if (inst.op !== "make_closure") return; + const child = funcsByName.get(inst.imms["fn"] as string); + if (child && !tainted.has(child)) { + tainted.add(child); + grew = true; + } + }); + } + if (!grew) break; + } +} + +// splice the boundary wrapper into fn's entry: a fresh entry block takes +// over the calling-convention params, one has_tag(number) guard per +// formal chains toward the fast block (all-number: unbox, call_typed +// the clone, return its boxed result), and any guard failure branches +// to the original entry — the untouched generic body. +function installWrapper(fn: Func, spec: SpecMode): void { + const oldEntry = fn.entry!; + const formals = oldEntry.params.slice(2); // [0]=%env [1]=%this + + const newEntry = new Block(fn, "wrapentry"); + newEntry.params = oldEntry.params; + for (const p of newEntry.params) p.block = newEntry; + oldEntry.params = []; + + const guards: Block[] = [newEntry]; + for (let i = 1; i < formals.length; i++) guards.push(new Block(fn, "wrapguard")); + const fast = new Block(fn, "wrapfast"); + for (const b of [...guards, fast]) b.sealed = true; + + for (let i = 0; i < formals.length; i++) { + const b = guards[i]!; + const t = new Inst(fn, "has_tag", [formals[i]!], { tag: "number" }); + const br = new Inst(fn, "cond_br", [t], {}); + for (const inst of [t, br]) { + inst.block = b; + b.insts.push(inst); + } + br.addTarget(i + 1 < formals.length ? guards[i + 1]! : fast, []); + br.addTarget(oldEntry, []); + } + + // the clone's ABI carries neither env nor `this` (post-checks + // guarantee both unused); the EIR-level env operand is never emitted + const envArg = new Inst(fn, "const", [], { kind: "undefined" }); + const unboxed = formals.map((f) => new Inst(fn, "unbox_f64", [f], {})); + const call = new Inst(fn, "call_typed", [envArg, ...unboxed], { fn: spec.cloneName }); + const ret = new Inst(fn, "return", [call], {}); + for (const inst of [envArg, ...unboxed, call, ret]) { + inst.block = fast; + fast.insts.push(inst); + } + + fn.blocks.splice(0, 0, ...guards, fast); + fn.entry = newEntry; +} + +export function specializeModule( + m: Module, + analysis: ScopeAnalysis, + oracle: TypeOracle, + this_module_info: { exports: Map } | null, + mod_ctx: ModCtx, + stats: SpecStats +): boolean { + // to a fixpoint: a freshly-lowered clone's body contains generic call + // sites of OTHER specializable functions (sum$typed still calls + // hypot2 through its slot) — each round re-enumerates over the module + // as it now stands and rewrites what became visible. `cloned` + // remembers per-function outcomes (SpecMode = clone shipped, null = + // clone rejected) so later rounds only add rewrites; `wrapped` + // remembers wrapper judgments (installed or declined) — a wrapper + // adds no rewritable sites, so one judgment is final. + const cloned = new Map(); + const wrapped = new Map(); + // escape taint persists across rounds (wrapper clones join at + // minting time); fencedSeen keeps the fence count per-site + const tainted = new Set(); + const fencedSeen = new Set(); + let changedAny = false; + for (let round = 0; round < 5; round++) { + if ( + !specializeRound( + m, + analysis, + oracle, + this_module_info, + mod_ctx, + stats, + cloned, + wrapped, + tainted, + fencedSeen + ) + ) + break; + changedAny = true; + } + return changedAny; +} + +function specializeRound( + m: Module, + analysis: ScopeAnalysis, + oracle: TypeOracle, + this_module_info: { exports: Map } | null, + mod_ctx: ModCtx, + stats: SpecStats, + cloned: Map, + wrapped: Map, + tainted: Set, + fencedSeen: Set +): boolean { + const uses = usesInModule(m); + let toplevelFn: Func | null = null; + for (const info of analysis.fnInfos.values()) + if (info.isToplevel && info.fn) toplevelFn = info.fn; + + const funcsByName = new Map(); + for (const fn of m.functions) funcsByName.set(fn.name, fn); + + // %self slot -> stores/loads, and slot -> promoted? + const selfStores = new Map(); + const selfLoads = new Map(); + for (const fn of m.functions) { + fn.forEachInst((inst) => { + if (inst.imms["module"] !== "%self") return; + const slot = inst.imms["slot"] as number; + if (inst.op === "module_slot_store") { + let l = selfStores.get(slot); + if (!l) selfStores.set(slot, (l = [])); + l.push(inst); + } else if (inst.op === "module_slot_load") { + let l = selfLoads.get(slot); + if (!l) selfLoads.set(slot, (l = [])); + l.push(inst); + } + }); + } + const promotedSlots = new Set(); + if (this_module_info) + this_module_info.exports.forEach((info) => { + if (info.promoted) promotedSlots.add(info.slot_num); + }); + + // per-function dominator trees, built lazily (only for functions that + // actually host slot-load rewrites) + const idoms = new Map>(); + const idomOf = (fn: Func): Map => { + let d = idoms.get(fn); + if (!d) { + const { rpo } = computeRPO(fn); + idoms.set(fn, (d = computeDominators(fn, rpo))); + } + return d; + }; + + // a plain closure-dispatch call using `v` as its callee? + const calleeUse = (u: Use): boolean => + u.user.op === "call" && u.operandIndex === 0 && !u.user.imms["direct"]; + + // one pass over the module: every make_closure, indexed by callee name + const closuresByName = new Map(); + for (const fn of m.functions) { + fn.forEachInst((inst) => { + if (inst.op !== "make_closure") return; + const name = inst.imms["fn"] as string; + let l = closuresByName.get(name); + if (!l) closuresByName.set(name, (l = [])); + l.push(inst); + }); + } + + // --- structural closure-flow (oracle-free), for every function ------- + // every flow of the closure value must end in a plain-call callee (or + // the single store into a promoted module-private slot); anything + // else is an escape. + const closureFlowOf = (info: FnInfo): ClosureFlow => { + const closures = closuresByName.get(info.name) || []; + if (closures.length === 0) return { referenced: false, escapes: false, sites: [] }; + + const sites: CallSite[] = []; + for (const c of closures) { + for (const u of uses.get(c) || []) { + if (u.operandIndex === -1) + return { referenced: true, escapes: true, sites }; // edge arg + if (calleeUse(u)) { + sites.push({ + call: u.user, + fn: u.fn, + rewritable: !(u.user.targets && u.user.targets.length > 0), + }); + continue; + } + // the one non-callee flow we can fully enumerate: the + // single store into a promoted module-private slot + if ( + u.user.op === "module_slot_store" && + u.user.imms["module"] === "%self" && + u.operandIndex === 0 + ) { + const slot = u.user.imms["slot"] as number; + const stores = selfStores.get(slot) || []; + if (!promotedSlots.has(slot) || stores.length !== 1 || stores[0] !== u.user) + return { referenced: true, escapes: true, sites }; + const store = u.user; + const storeFn = u.fn; + // a store in the toplevel ENTRY block with no + // CALL-effect instruction before it is + // cross-function-safe: no user code can run before + // the slot is initialized, so no load anywhere can + // observe the pre-store state — except a load + // TEXTUALLY earlier in the entry block itself, which + // reads the uninitialized slot (the documented + // hoisting-lost semantics) and must stay generic. + let prefixSafe = false; + if (toplevelFn && storeFn === toplevelFn && store.block === toplevelFn.entry) { + prefixSafe = true; + for (const inst of toplevelFn.entry!.insts) { + if (inst === store) break; + if ((opInfo(inst.op).effects & Effect.CALL) !== 0) { + prefixSafe = false; + break; + } + } + } + for (const load of selfLoads.get(slot) || []) { + for (const lu of uses.get(load) || []) { + if (lu.operandIndex === -1 || !calleeUse(lu)) + return { referenced: true, escapes: true, sites }; + // rewrite where the load provably yields this + // closure: after a prefix-safe store, any load + // except one earlier in the same entry block; + // otherwise same-function store-dominated only + // (elsewhere the generic slot path stands) + const loadFn = fnOf(load); + const orderOk = prefixSafe + ? !( + load.block === store.block && + store.block!.insts.indexOf(load) < + store.block!.insts.indexOf(store) + ) + : loadFn === storeFn && + comesBefore(idomOf(storeFn), store, load); + const rewritable = + orderOk && !(lu.user.targets && lu.user.targets.length > 0); + sites.push({ call: lu.user, fn: lu.fn, rewritable }); + } + } + continue; + } + return { referenced: true, escapes: true, sites }; + } + } + return { referenced: true, escapes: false, sites }; + }; + + const flows = new Map(); + for (const info of analysis.fnInfos.values()) { + if (info.isToplevel || !info.lowered || !info.fn) continue; + flows.set(info, closureFlowOf(info)); + } + + computeEscapeTaint(m, flows, funcsByName, tainted); + + let changed = false; + + for (const [info, flow] of flows) { + const node = info.node; + + // ===== escaping: the boundary-wrapper path ======================= + // (merely TAINTED functions — called from tainted hosts but not + // escaping themselves — stay on the trusted path below: their + // clones are entered only through rewritten sites in covered + // code, and the fence keeps tainted-hosted sites generic. + // Wrappers for tainted-called internal helpers / guarded + // per-site dispatch are the recorded follow-on.) + if (flow.escapes) { + if (wrapped.has(info)) continue; // judged (installed or declined) + if (!passes().exportWrapper) continue; + if (!flow.referenced) continue; + + // static callee checks (AST side); >=1 formal or the guard + // chain guards nothing + if (info.restBinding || info.usesArguments) continue; + if ((info.defaults || []).some((d) => d != null)) continue; + if (!node.params.every((p) => p.type === "Identifier")) continue; + if (node.params.length === 0) continue; + // heuristic only (guards decide): skip formals the oracle + // POSITIVELY types non-number — the chain could never pass + if (!node.params.every((p) => nodeMayBeNumber(oracle, p))) continue; + // the entry must own the calling convention outright + if (info.fn!.entry!.predEdges.length > 0) continue; + + const spec: SpecMode = { + cloneName: uniqueCloneName(m, `${info.name}$wrap`), + trusted: false, + formals: node.params.map(() => "f64" as const), + result: "any", + }; + const diamondsBefore = mod_ctx.typed_stats ? mod_ctx.typed_stats.diamonds : 0; + const clone = lowerSpecializedClone(info, analysis, m, mod_ctx, spec); + const diamondsAfter = mod_ctx.typed_stats ? mod_ctx.typed_stats.diamonds : 0; + + // structural post-checks: the unboxed ABI carries neither env + // nor `this`, and no frame ops. Returns stay boxed ("any"), + // so no return check. Payoff check: a clone that emitted no + // diamonds has nothing for the entry proofs to fold. + const cloneUses = new Map(); + let ok = true; + clone.forEachInst((inst) => { + if (CLONE_FRAME_OPS.has(inst.op)) ok = false; + for (const o of inst.operands) cloneUses.set(o, (cloneUses.get(o) || 0) + 1); + if (inst.targets) + for (const t of inst.targets) + for (const a of t.args) + if (a) cloneUses.set(a, (cloneUses.get(a) || 0) + 1); + }); + const envParam = clone.entry!.params[0]!; + const thisParam = clone.entry!.params[1]!; + if ((cloneUses.get(envParam) || 0) > 0) ok = false; + if ((cloneUses.get(thisParam) || 0) > 0) ok = false; + if (!ok || diamondsAfter - diamondsBefore === 0) { + stats.rejected++; + wrapped.set(info, false); + continue; + } + m.addFunction(clone); + tainted.add(clone); // its activations ARE the external entries + installWrapper(info.fn!, spec); + stats.wrapped++; + wrapped.set(info, true); + changed = true; + continue; + } + + // ===== covered (analysis-complete): the trusted path ============= + + // a candidate this call already judged: null = clone was rejected + // (don't re-lower it every round); a SpecMode = clone exists, only + // NEW call sites (in later-lowered clone bodies) need rewriting + const priorSpec = cloned.get(info); + if (priorSpec === null) continue; + + if (priorSpec === undefined) { + // --- static callee checks (AST side) ----------------------------- + if (info.restBinding || info.usesArguments) continue; + if ((info.defaults || []).some((d) => d != null)) continue; + if (!node.params.every((p) => p.type === "Identifier")) continue; + + // --- type profile: every formal and return exactly {number} ------ + if (!node.params.every((p) => nodeIsNumber(oracle, p))) continue; + const returns = ownReturns(node); + if (returns.length === 0) continue; + if (!returns.every((r) => r.argument && nodeIsNumber(oracle, r.argument))) continue; + } + + if (!flow.referenced || flow.escapes || flow.sites.length === 0) continue; + const sites = flow.sites; + + // don't mint a clone no covered site will ever call (later rounds + // re-judge: freshly-lowered covered clones can add sites) + if (!priorSpec && !sites.some((s) => s.rewritable && !tainted.has(s.fn))) continue; + + let spec: SpecMode; + if (priorSpec) { + spec = priorSpec; // clone already shipped in an earlier round + } else { + // --- lower the clone (unguarded body, typed sig) ---------------- + spec = { + cloneName: uniqueCloneName(m, `${info.name}$typed`), + trusted: true, + formals: node.params.map(() => "f64" as const), + result: "f64", + }; + const clone = lowerSpecializedClone(info, analysis, m, mod_ctx, spec); + + // --- structural post-checks (trust-free backstop) --------------- + // the clone must actually honor the signature: env/this unused, + // no frame ops, every return raw f64. anything else discards it. + const cloneUses = new Map(); + let ok = true; + clone.forEachInst((inst) => { + if (CLONE_FRAME_OPS.has(inst.op)) ok = false; + if (inst.op === "return" && inst.operands[0]!.type !== "f64") ok = false; + for (const o of inst.operands) cloneUses.set(o, (cloneUses.get(o) || 0) + 1); + if (inst.targets) + for (const t of inst.targets) + for (const a of t.args) + if (a) cloneUses.set(a, (cloneUses.get(a) || 0) + 1); + }); + const envParam = clone.entry!.params[0]!; + const thisParam = clone.entry!.params[1]!; + // the entry box_f64 of each formal is the formal's ONLY allowed + // use shape; env/this must be entirely unused + if ((cloneUses.get(envParam) || 0) > 0) ok = false; + if ((cloneUses.get(thisParam) || 0) > 0) ok = false; + if (!ok) { + stats.rejected++; + cloned.set(info, null); + continue; + } + m.addFunction(clone); + cloned.set(info, spec); + stats.specialized++; + changed = true; + } + + // --- rewrite the provably-known call sites -------------------------- + for (const site of sites) { + if (!site.rewritable) continue; + // the escape-taint fence: a site hosted in tainted code sees + // values the analysis never covered — the generic call stands + if (tainted.has(site.fn)) { + if (!fencedSeen.has(site.call)) { + fencedSeen.add(site.call); + stats.fenced++; + } + continue; + } + const call = site.call; + const g = site.fn; + const args = call.operands.slice(2); + if (args.length !== spec.formals.length) continue; + const block = call.block!; + const at = block.insts.indexOf(call); + if (at < 0) continue; + + const insts: Inst[] = []; + const envArg = new Inst(g, "const", [], { kind: "undefined" }); + insts.push(envArg); + const unboxed = args.map((a) => { + const u = new Inst(g, "unbox_f64", [a], {}); + insts.push(u); + return u; + }); + const direct = new Inst(g, "call_typed", [envArg, ...unboxed], { + fn: spec.cloneName, + }); + direct.type = "f64"; + insts.push(direct); + const boxed = new Inst(g, "box_f64", [direct], {}); + insts.push(boxed); + for (const i of insts) i.block = block; + block.insts.splice(at, 0, ...insts); + + // point every consumer at the re-boxed result, then drop the + // generic call (its callee/`this` operands lose their last use + // and fall to DCE where possible) + replaceCallWith(g, call, boxed); + stats.sites++; + changed = true; + } + } + return changed; +} + +function replaceCallWith(fn: Func, call: Inst, replacement: Inst): void { + fn.forEachInst((inst) => { + if (inst === replacement) return; + for (let i = 0; i < inst.operands.length; i++) + if (inst.operands[i] === call) inst.operands[i] = replacement; + if (inst.targets) + for (const t of inst.targets) + for (let i = 0; i < t.args.length; i++) + if (t.args[i] === call) t.args[i] = replacement; + }); + const b = call.block!; + const idx = b.insts.indexOf(call); + if (idx >= 0) b.insts.splice(idx, 1); + call.block = null; +} diff --git a/lib/eir/tests.ts b/lib/eir/tests.ts new file mode 100644 index 00000000..aa2713c1 --- /dev/null +++ b/lib/eir/tests.ts @@ -0,0 +1,3909 @@ +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: + */ + +// EIR unit tests. run (via the tsjs+babel tree) with: +// node lib/generated/lib/eir/tests.js +// or through buck: +// buck2 build //:test-eir + +import { FunctionBuilder } from "./builder"; +import { printFunction, printModule } from "./printer"; +import { verifyFunction, verifyModule } from "./verifier"; +import { lowerFunctionNode, lowerProgram, lowerAnalyzedFunction } from "./lower"; +import { optimizeFunction, optimizeModule } from "./optimize"; +import type { OptStats } from "./optimize"; +import { devirtualizeModule } from "./devirt"; +import { specializeModule } from "./specialize"; +import { ScopeAnalysis } from "./scopes"; +import { isLowerNotSupported } from "./errors"; +import { Func, Block, Inst, Module } from "./ir"; +import { DesugarSpread } from "../passes/desugar-spread"; +import { typeSigToEirType, typeSigToShapeRepr } from "./oracle"; +import type { OracleShapeField, TypeOracle, TypeTag } from "./oracle"; +import { optimizeShapeRegions } from "./optimize-guards"; +import { sinkConstructResults } from "./sink-construct"; +import { buildArithDiamond, buildLowTierAdd, buildLowTierLt } from "./lowtier-probe"; +import { DesugarClasses } from "../passes/desugar-classes"; +import { DesugarDestructuring } from "../passes/desugar-destructuring"; +import { DesugarGeneratorFunctions } from "../passes/desugar-generator-functions"; +import { DesugarMetaProperties } from "../passes/desugar-metaproperties"; +import * as esprima from "../../external-deps/esprima/esprima-es6"; +import type * as e from "../estree"; +import type { CompilerOptions } from "../options"; +import { withPassConfig } from "../pass-config"; + +let failures = 0; + +function test(name: string, fn: () => void): void { + try { + fn(); + console.log(`pass: ${name}`); + } catch (err) { + failures++; + const failure = err as Error; + console.log(`FAIL: ${name}: ${failure.message}`); + if (failure.stack) console.log(failure.stack.split("\n").slice(1, 4).join("\n")); + } +} + +function assert(cond: boolean, msg?: string): void { + if (!cond) throw new Error(`assertion failed: ${msg || ""}`); +} + +function assertContains(haystack: string, needle: string): void { + if (haystack.indexOf(needle) === -1) + throw new Error(`expected output to contain '${needle}'\n---\n${haystack}\n---`); +} + +// op-exact matchers: `make_object` must not substring-match +// `make_object_shaped` (underscore is a word character, so \b after the +// op name rejects the longer op) +function containsOp(haystack: string, op: string): boolean { + return new RegExp("\\b" + op + "\\b").test(haystack); +} + +function assertContainsOp(haystack: string, op: string): void { + if (!containsOp(haystack, op)) + throw new Error(`expected output to contain op '${op}'\n---\n${haystack}\n---`); +} + +function assertNotContainsOp(haystack: string, op: string): void { + if (containsOp(haystack, op)) + throw new Error(`expected output to NOT contain op '${op}'\n---\n${haystack}\n---`); +} + +function findBlock(fn: Func, prefix: string): Block { + for (let b of fn.blocks) if (b.name.indexOf(prefix) === 0) return b; + throw new Error(`no block named ${prefix}* in @${fn.name}`); +} + +function findFn(mod: Module, name: string): Func { + for (let f of mod.functions) if (f.name === name) return f; + throw new Error(`no function @${name} in module`); +} + +function parseFn(src: string): e.FunctionDeclaration { + let ast = esprima.parse(src, { loc: true, raw: true }); + for (let s of ast.body) if (s.type === "FunctionDeclaration") return s; + throw new Error("no function declaration in source"); +} + +function lowerOne(src: string): { module: Module; fn: Func } { + let r = lowerFunctionNode(parseFn(src)); + verifyModule(r.module); + return r; +} + +// --- builder ------------------------------------------------------------------ + +test("builder: diamond join inserts a block param", () => { + let fb = new FunctionBuilder("diamond", ["c"]); + + let then_bb = fb.newBlock("then"); + let else_bb = fb.newBlock("else"); + let join_bb = fb.newBlock("join"); + + let cond = fb.readVariable("c", fb.cur); + fb.condBr(cond, then_bb, [], else_bb, []); + fb.sealBlock(then_bb); + fb.sealBlock(else_bb); + + fb.setInsertPoint(then_bb); + fb.writeVariable("x", then_bb, fb.constNumber(1)); + fb.br(join_bb, []); + + fb.setInsertPoint(else_bb); + fb.writeVariable("x", else_bb, fb.constNumber(2)); + fb.br(join_bb, []); + fb.sealBlock(join_bb); + + fb.setInsertPoint(join_bb); + let x = fb.readVariable("x", join_bb); + fb.ret(x); + + let fn = fb.finish(); + verifyFunction(fn); + + assert(x.op === "blockparam", "join read should be a block param"); + assert(findBlock(fn, "join").params.length === 1, "join should have exactly one param"); +}); + +test("builder: same value in both arms leaves no param", () => { + let fb = new FunctionBuilder("nodifference", ["c"]); + + let v = fb.constNumber(42); + fb.writeVariable("x", fb.cur, v); + + let then_bb = fb.newBlock("then"); + let else_bb = fb.newBlock("else"); + let join_bb = fb.newBlock("join"); + + fb.condBr(fb.readVariable("c", fb.cur), then_bb, [], else_bb, []); + fb.sealBlock(then_bb); + fb.sealBlock(else_bb); + + fb.setInsertPoint(then_bb); + fb.br(join_bb, []); + fb.setInsertPoint(else_bb); + fb.br(join_bb, []); + fb.sealBlock(join_bb); + + fb.setInsertPoint(join_bb); + let x = fb.readVariable("x", join_bb); + fb.ret(x); + + let fn = fb.finish(); + verifyFunction(fn); + assert(x === v, "read through the join should see the original value"); + assert(findBlock(fn, "join").params.length === 0, "no params expected at join"); +}); + +// --- lowering: SSA shapes ------------------------------------------------------- + +test("lower: loop-invariant variable gets no header param", () => { + let { fn } = lowerOne("function f(c) { let a = 5; while (c) { } return a; }"); + assert(findBlock(fn, "while_header").params.length === 0, "header should have no params"); +}); + +test("lower: loop counter gets exactly one header param", () => { + let { fn } = lowerOne( + "function g(n) { let i = 0; while (i < n) { i = i + 1; } return i; }" + ); + let header = findBlock(fn, "while_header"); + assert(header.params.length === 1, `header params = ${header.params.length}`); +}); + +test("lower: if/else assigns and joins", () => { + let { fn } = lowerOne( + "function h(c) { let x = 0; if (c) { x = 1; } else { x = 2; } return x; }" + ); + assert(findBlock(fn, "if_join").params.length === 1, "join should have one param"); +}); + +test("lower: logical && short-circuits through a join param", () => { + let { fn } = lowerOne("function a(x, y) { return x && y; }"); + let printed = printFunction(fn); + assertContains(printed, "logical_join"); + assertContains(printed, "cond_br"); +}); + +test("lower: method calls and property access", () => { + let { fn } = lowerOne( + "function m(o) { o.count = o.count + 1; return o.get(o.count, 3); }" + ); + let printed = printFunction(fn); + assertContains(printed, 'get_prop_atom'); + assertContains(printed, 'atom="count"'); + assertContains(printed, 'set_prop_atom'); + assertContains(printed, "call"); +}); + +test("lower: globals resolve to get_global", () => { + let { fn } = lowerOne("function p(x) { return console.log(x); }"); + assertContains(printFunction(fn), 'get_global atom="console"'); +}); + +test("lower: for loop with break/continue", () => { + let { fn } = lowerOne( + "function bc(n) { let s = 0; " + + "for (let i = 0; i < n; i = i + 1) { " + + "if (i === 3) continue; if (i === 7) break; s = s + i; } " + + "return s; }" + ); + let header = findBlock(fn, "for_header"); + assert(header.params.length === 2, `header params = ${header.params.length} (want s, i)`); +}); + +test("lower: do-while", () => { + let { fn } = lowerOne( + "function dw(n) { let i = 0; do { i = i + 1; } while (i < n); return i; }" + ); + findBlock(fn, "do_body"); + findBlock(fn, "do_cond"); +}); + +// --- lowering: closures / environments -------------------------------------------- + +test("lower: closure counter allocates an env and captures", () => { + let { module, fn } = lowerOne( + "function outer() { let c = 0; function inc() { c = c + 1; return c; } return inc; }" + ); + assert(module.functions.length === 2, "module should have outer + inc"); + + let printed_outer = printFunction(fn); + assertContains(printed_outer, "make_env size=1"); + assertContains(printed_outer, "env_store"); + assertContains(printed_outer, 'make_closure'); + assertContains(printed_outer, 'fn="outer.inc"'); + + let inc = findFn(module, "outer.inc"); + let printed_inc = printFunction(inc); + assertContains(printed_inc, "env_load"); + assertContains(printed_inc, "env_store"); +}); + +test("lower: capture through an env-less intermediate function", () => { + let { module } = lowerOne( + "function o() { let x = 1; " + + "function mid() { function inner() { return x; } return inner; } " + + "return mid; }" + ); + assert(module.functions.length === 3, "module should have o, mid, inner"); + + // mid captures nothing itself: no env of its own, it forwards its + // incoming env to inner's closure + let mid = findFn(module, "o.mid"); + let printed_mid = printFunction(mid); + assert(printed_mid.indexOf("make_env") === -1, "mid should not allocate an env"); + assertContains(printed_mid, "make_closure"); + + // inner reads x straight out of its incoming env (zero hops) + let inner = findFn(module, "o.mid.inner"); + let printed_inner = printFunction(inner); + assertContains(printed_inner, "env_load"); + assert(printed_inner.indexOf("slot=0") !== -1, "x should live in slot 0 of o's env"); +}); + +test("lower: captured parameter is stored to the env at entry", () => { + let { fn } = lowerOne( + "function k(x) { function get() { return x; } return get; }" + ); + let printed = printFunction(fn); + assertContains(printed, "make_env size=1"); + assertContains(printed, "env_store"); +}); + +test("lower: function expressions become closures", () => { + let { module, fn } = lowerOne( + "function fe() { let f = function (a) { return a + 1; }; return f(2); }" + ); + assert(module.functions.length === 2, "module should have fe + anon"); + assertContains(printFunction(fn), "make_closure"); +}); + +// --- lowering: exceptions --------------------------------------------------------- + +test("lower: try/catch produces unwind edges into a catch block", () => { + let { fn } = lowerOne( + "function t(o) { try { o.f(); } catch (e) { return e; } return 1; }" + ); + let printed = printFunction(fn); + assertContains(printed, "unwind ^catch"); + assertContains(printed, "normal ^cont"); + assertContains(printed, ": exception):"); + + let catch_bb = findBlock(fn, "catch"); + assert(catch_bb.isCatch, "catch block should be marked"); + assert(catch_bb.params[0]!.isException, "first catch param is the exception"); +}); + +test("lower: throw inside try unwinds to the local handler", () => { + let { fn } = lowerOne( + "function th(c) { try { if (c) throw c; } catch (e) { return e; } return 0; }" + ); + let printed = printFunction(fn); + assertContains(printed, "throw"); + assertContains(printed, "unwind ^catch"); +}); + +test("lower: variable state joins into the catch block per throw site", () => { + let { fn } = lowerOne( + "function j(o) { let x = 1; try { o.a(); x = 2; o.b(); } catch (e) { return x; } return x; }" + ); + // catch reads x: its value differs by which call threw, so the catch + // block needs a (non-exception) param joining 1 and 2. + let catch_bb = findBlock(fn, "catch"); + assert( + catch_bb.params.length === 2, + `catch should have exception + x params, got ${catch_bb.params.length}` + ); +}); + +// --- lowering: misc ------------------------------------------------------------------ + +test("lower: new expressions become construct", () => { + let { fn } = lowerOne("function nw(C) { return new C(1, 2); }"); + assertContains(printFunction(fn), "construct"); +}); + +test("lower: array and object literals", () => { + let { fn } = lowerOne("function lit() { return [1, 2, { a: 3, b: 4 }]; }"); + let printed = printFunction(fn); + assertContains(printed, "make_array"); + // static-key literals are born with their shape (all-boxed reprs + // without an oracle) + assertContainsOp(printed, "make_object_shaped"); + assertContains(printed, 'shape="a:boxed,b:boxed"'); +}); + +test("lower: this expression", () => { + let { fn } = lowerOne("function tt() { return this.x; }"); + assertContains(printFunction(fn), "get_prop_atom"); +}); + +test("lower: unsupported constructs raise LowerNotSupported", () => { + let threw = false; + try { + lowerFunctionNode(parseFn("function t(x) { with (x) { return y; } }")); + } catch (e) { + threw = isLowerNotSupported(e); + } + assert(threw, "expected LowerNotSupported"); +}); + +// the remaining source-reachable LowerNotSupported guards. everything +// else in scopes.js/lower.js is defensive: either the parser rejects the +// construct outright or a pre-EIR desugar pass removes it before EIR +// sees it. these are the constructs a user can actually write that +// don't lower — each must fail loudly (there is no fallback pipeline). +test("lower: delete of a variable raises LowerNotSupported", () => { + let threw = false; + try { + lowerFunctionNode(parseFn("function t(x) { delete x; return 1; }")); + } catch (e) { + threw = isLowerNotSupported(e); + } + assert(threw, "expected LowerNotSupported"); +}); + +// --- lowering: per-iteration loop envs --------------------------------------- + +test("lower: captured for-let var gets a per-iteration env", () => { + let { module, fn } = lowerOne( + "function f() { let fns = []; for (let i = 0; i < 3; i++) { fns.push(function () { return i; }); } return fns; }" + ); + let printed = printFunction(fn); + // an env is created at loop entry AND refreshed in the update block + let update = findBlock(fn, "for_update"); + assert( + update.insts.some((i) => i.op === "make_env"), + "update block should make a fresh env" + ); + // the header carries the current env as a block param + assert(findBlock(fn, "for_header").params.length === 1, "header should carry the env"); + // the closure reads the loop var from its incoming env + let child = findFn(module, "f.anon0"); + assertContains(printFunction(child), "env_load"); + verifyModule(module); +}); + +test("lower: uncaptured for-let vars stay SSA (no loop env)", () => { + let { fn } = lowerOne( + "function f(n) { let sum = 0; for (let i = 0; i < n; i++) { sum = sum + i; } return sum; }" + ); + assert( + !printFunction(fn).includes("make_env"), + "no env expected for uncaptured loop vars" + ); +}); + +test("lower: captured for-of var gets a fresh env per iteration", () => { + let { module, fn } = lowerOne( + "function f(xs) { let fns = []; for (let x of xs) { fns.push(function () { return x; }); } return fns; }" + ); + let body = findBlock(fn, "forof_body"); + assert( + body.insts.some((i) => i.op === "make_env"), + "body should make a fresh env each iteration" + ); + verifyModule(module); +}); + +test("lower: for-of RHS closure capturing the loop var sees the loop env", () => { + // scope analysis declares the binding before walking the RHS, so the + // closure's incoming env must be the loop env (holding undefined at + // that point — echojs has no TDZ), not the function env + let { module, fn } = lowerOne( + "function f(mk) { let fns = []; for (let x of mk(function () { return x; })) { fns.push(function () { return x; }); } return fns; }" + ); + // an initial env exists before the RHS call + const entry = fn.blocks[0]!; + assert( + entry.insts.some((i) => i.op === "make_env"), + "entry should create the initial loop env before the RHS evaluates" + ); + verifyModule(module); +}); + +test("lower: nested captured loops chain their envs", () => { + let { module } = lowerOne( + "function f(base) { let fns = []; for (let i = 0; i < 2; i++) { for (let j = 0; j < 2; j++) { fns.push(function () { return base + i + j; }); } } return fns; }" + ); + verifyModule(module); // the env chain must verify (dominance + slots) +}); + +// --- lowering: %-intrinsics ------------------------------------------------ + +// parse + the pre-EIR desugar passes, like preEIRConvert in compile() +function parseFnPreEIR(src: string): e.FunctionDeclaration { + let ast = esprima.parse(src, { loc: true, raw: true }); + const opts = { debug_passes: new Set() } as CompilerOptions; + ast = new DesugarClasses(opts).visit(ast) as e.Program; + ast = new DesugarDestructuring(opts).visit(ast) as e.Program; + ast = new DesugarGeneratorFunctions(opts).visit(ast) as e.Program; + ast = new DesugarSpread(opts).visit(ast) as e.Program; + ast = new DesugarMetaProperties(opts).visit(ast) as e.Program; + for (const s of ast.body) if (s.type === "FunctionDeclaration") return s; + throw new Error("no function declaration in source"); +} +let parseFnSpreadDesugared = parseFnPreEIR; + +test("lower: spread call lowers via %arrayFromSpread -> array_from_spread", () => { + let r = lowerFunctionNode(parseFnSpreadDesugared("function f(a) { return g(1, 2, ...a); }")); + verifyModule(r.module); + let printed = printFunction(r.fn); + assertContains(printed, "array_from_spread"); + assert(printed.indexOf('get_global atom="%') === -1, "intrinsic leaked as a global load"); +}); + +test("lower: array literal spread lowers to array_from_spread", () => { + let r = lowerFunctionNode(parseFnSpreadDesugared("function f(a, b) { return [0, ...a, ...b]; }")); + verifyModule(r.module); + assertContains(printFunction(r.fn), "array_from_spread"); +}); + +test("lower: computed accessor keys lower via define_accessor_computed", () => { + let r = lowerFunctionNode( + parseFnPreEIR( + "function t(k) { return { get [k]() { return 1; }, set [k](v) { this.v = v; } }; }" + ) + ); + verifyModule(r.module); + let printed = printFunction(r.fn); + let first = printed.indexOf("define_accessor_computed"); + assert(first !== -1, "expected define_accessor_computed"); + assert( + printed.indexOf("define_accessor_computed", first + 1) !== -1, + "expected separate defines for getter and setter" + ); +}); + +test("lower: for-of pattern heads desugar pre-EIR", () => { + let r = lowerFunctionNode( + parseFnPreEIR("function f(ps) { let r = 0; for (let [a, b] of ps) r = r + a * b; return r; }") + ); + verifyModule(r.module); +}); + +test("lower: catch parameter patterns desugar pre-EIR", () => { + let r = lowerFunctionNode( + parseFnPreEIR( + "function f(g) { try { return g(); } catch ({ message }) { return message; } }" + ) + ); + verifyModule(r.module); +}); + +test("lower: nested array spread lowers", () => { + let r = lowerFunctionNode( + parseFnPreEIR("function f(xs) { return [...[...xs, 5], 6]; }") + ); + verifyModule(r.module); + let printed = printFunction(r.fn); + let first = printed.indexOf("array_from_spread"); + assert(first !== -1, "expected array_from_spread"); + assert(printed.indexOf("array_from_spread", first + 1) !== -1, "expected a second array_from_spread for the nested spread"); +}); + +test("lower: debugger statement is a no-op", () => { + let r = lowerFunctionNode(parseFn("function f() { debugger; return 1; }")); + verifyModule(r.module); +}); + +test("lower: unknown %-intrinsics raise LowerNotSupported", () => { + let fnNode = parseFnSpreadDesugared("function t(a) { return dummy(a); }"); + // synthesize a call to an intrinsic lowering doesn't know + const retstmt = fnNode.body.body[0] as e.ReturnStatement; + ((retstmt.argument as e.CallExpression).callee as e.Identifier).name = "%noSuchIntrinsic"; + let threw = false; + try { + lowerFunctionNode(fnNode); + } catch (e) { + threw = isLowerNotSupported(e); + } + assert(threw, "expected LowerNotSupported"); +}); + +test("lower: derived class ctor lowers construct_super and rebinds this", () => { + let r = lowerFunctionNode( + parseFnPreEIR( + "function f(v) { class A { constructor(x) { this.x = x; } } class B extends A { constructor() { super(1); this.v = v; } } return new B(); }" + ) + ); + verifyModule(r.module); + let ctor: Func | null = null; + for (const fn of r.module.functions) if (/\.B$/.test(fn.name)) ctor = fn; + assert(!!ctor, "expected the B constructor in the module"); + let printed = printFunction(ctor!); + assertContains(printed, "construct_super"); + // this.v = v must store into construct_super's result, not the entry + // this param (%1) + assert(!/set_prop_atom %1,/.test(printed), "post-super `this` should be the rebound value"); +}); + +test("lower: spread super call lowers to construct_super_apply", () => { + let r = lowerFunctionNode( + parseFnPreEIR( + "function f() { class A { constructor(a, b) { this.s = a + b; } } class B extends A { constructor(xs) { super(...xs); } } return new B([1, 2]); }" + ) + ); + verifyModule(r.module); + let all = r.module.functions.map((fn) => printFunction(fn)).join("\n"); + assertContains(all, "construct_super_apply"); +}); + +test("lower: new.target lowers to new_target", () => { + let r = lowerFunctionNode(parseFnPreEIR("function f() { return new.target; }")); + verifyModule(r.module); + assertContains(printFunction(r.fn), "new_target"); +}); + +test("lower: class accessors lower via make_object_shaped + defineProperties", () => { + let r = lowerFunctionNode( + parseFnPreEIR( + "function f() { class T { get n() { return 1; } set n(v) { this._x = v; } } return new T(); }" + ) + ); + verifyModule(r.module); + let all = r.module.functions.map((fn) => printFunction(fn)).join("\n"); + // one property entry carrying BOTH accessors (the get/set pair shares + // a descriptor literal with fields get,set) + assertContains(all, 'shape="get:boxed,set:boxed"'); + assertContains(all, 'atom="defineProperties"'); +}); + +test("lower: array destructuring lowers via %createIteratorWrapper", () => { + let r = lowerFunctionNode( + parseFnPreEIR("function f(xs) { let [a, ...rest] = xs; return a + rest.length; }") + ); + verifyModule(r.module); + assertContains(printFunction(r.fn), 'name="iterator_wrapper_new"'); +}); + +test("lower: pattern defaults lower as undefined checks", () => { + let r = lowerFunctionNode( + parseFnPreEIR("function f(o) { let { a = 5 } = o; return a; }") + ); + verifyModule(r.module); + let printed = printFunction(r.fn); + assertContains(printed, "strict_eq"); + assertContains(printed, "get_prop_atom"); +}); + +test("lower: unary void evaluates its argument and yields undefined", () => { + let { fn } = lowerOne("function f(g) { return void g(); }"); + let printed = printFunction(fn); + assertContains(printed, "call"); + assertContains(printed, 'kind="undefined"'); +}); + +test("lower: arrow lexical this reads the owner's captured this", () => { + let { module, fn } = lowerOne( + "function f() { return function () { return [1].map(() => this.x); }; }" + ); + verifyModule(module); + // the method stores its this into an env; the arrow env_loads it + let method: Func | null = null; + for (const g of module.functions) if (/anon0$/.test(g.name)) method = g; + assert(!!method, "expected the method in the module"); + assertContains(printFunction(method!), "env_store"); + let arrow: Func | null = null; + for (const g of module.functions) if (/arrow1$/.test(g.name)) arrow = g; + assert(!!arrow, "expected the arrow in the module"); + assertContains(printFunction(arrow!), "env_load"); +}); + +test("lower: toplevel-arrow candidates using this still fall back", () => { + const ast = esprima.parse("var f = () => this.x;", { loc: true, raw: true }); + const decl = ast.body[0] as e.VariableDeclaration; + const arrow = decl.declarations[0]!.init as e.ArrowFunctionExpression; + let threw = false; + try { + lowerFunctionNode(arrow, "f"); + } catch (e) { + threw = isLowerNotSupported(e); + } + assert(threw, "expected LowerNotSupported"); +}); + +test("lower: captured body-block let gets a per-iteration env", () => { + let { module, fn } = lowerOne( + "function f(n) { let fns = []; for (var i = 0; i < n; i++) { let j = i; fns.push(function () { return j; }); } return fns; }" + ); + verifyModule(module); + let body = findBlock(fn, "for_body"); + assert( + body.insts.some((i) => i.op === "make_env"), + "loop body should make a fresh env each iteration" + ); +}); + +test("lower: array holes stay holes", () => { + let { fn } = lowerOne("function f() { return [, , 3]; }"); + let printed = printFunction(fn); + assertContains(printed, "len=3"); +}); + +test("lower: closures carry source-level display names", () => { + let { fn } = lowerOne("function f() { return function inner() {}; }"); + assertContains(printFunction(fn), 'name="inner"'); +}); + +test("scopes: same-named functions get distinct EIR names", () => { + let r = lowerFunctionNode( + parseFn( + "function f() { function g() {} let o = { m: function g() {} }; return o.m || g; }" + ) + ); + verifyModule(r.module); + let names = r.module.functions.map((x) => x.name); + assert(new Set(names).size === names.length, `duplicate names: ${names}`); +}); + +test("lower: labeled break/continue on nested loops", () => { + let { module, fn } = lowerOne( + "function f(g) { outer: for (let i = 0; i < 9; i++) { for (let j = 0; j < 9; j++) { if (g(i, j) < 0) break outer; if (g(i, j) > 9) continue outer; } } return 1; }" + ); + verifyModule(module); + assert(fn.blocks.length > 6, "expected nested loop CFG"); +}); + +test("lower: labeled non-loop statement with break", () => { + let { module, fn } = lowerOne( + "function f(x) { let r = 0; done: { r = 1; if (x) break done; r = 2; } return r; }" + ); + verifyModule(module); + let labelBlock: Block | null = null; + for (const blk of fn.blocks) if (blk.name.indexOf("label_done") === 0) labelBlock = blk; + assert(!!labelBlock, "expected the label exit block"); +}); + +test("lower: labeled continue through a finally runs the finalizer", () => { + let { module, fn } = lowerOne( + "function f(xs, log) { outer: for (let i = 0; i < xs.length; i++) { for (let j = 0; j < 2; j++) { try { if (xs[i] < 0) continue outer; } finally { log.push(j); } } } return log; }" + ); + verifyModule(module); + // finalizer duplication: the log.push lowers at least twice (normal + // path + the labeled-continue path) + let pushes = 0; + fn.forEachInst((inst) => { + if (inst.op === "get_prop_atom" && inst.imms.atom === "push") pushes++; + }); + assert(pushes >= 2, `expected duplicated finalizer, saw ${pushes} push loads`); +}); + +test("lower: object-literal accessors define get/set pairs together", () => { + let { module, fn } = lowerOne( + "function f(v) { let o = { a: 1, get n() { return v; }, set n(x) { v = x; } }; return o; }" + ); + verifyModule(module); + let defines = 0; + fn.forEachInst((inst) => { + if (inst.op === "define_accessor") { + defines++; + assert(inst.imms.atom === "n", "accessor key"); + } + }); + assert(defines === 1, `get/set pair must be ONE define_accessor, saw ${defines}`); +}); + +test("lower: tagged templates lower via template_callsite", () => { + let { module, fn } = lowerOne("function f(tag, x) { return tag`a ${x} b`; }"); + verifyModule(module); + let sites = 0; + fn.forEachInst((inst) => { + if (inst.op === "template_callsite") { + sites++; + assert((inst.imms["cooked"] as readonly string[]).length === 2, "two cooked strings"); + } + }); + assert(sites === 1, `expected one callsite, saw ${sites}`); +}); + +test("lower: generator function lowers via make_generator/generator_yield", () => { + let r = lowerFunctionNode( + parseFnPreEIR("function f() { function* g() { yield 1; yield 2; } return g(); }") + ); + verifyModule(r.module); + let all = r.module.functions.map((fn) => printFunction(fn)).join("\n"); + assertContains(all, 'name="make_generator"'); + assertContains(all, 'name="generator_yield"'); +}); + +test("lower: statement-position yield* lowers as a for-of delegate loop", () => { + let r = lowerFunctionNode( + parseFnPreEIR( + "function f() { function* inner() { yield 1; } function* outer() { yield* inner(); } return outer(); }" + ) + ); + verifyModule(r.module); + let all = r.module.functions.map((fn) => printFunction(fn)).join("\n"); + assertContains(all, 'name="generator_yield"'); +}); + +test("lower: program with several functions", () => { + let ast = esprima.parse( + "function one() { return 1; } function two() { return one() + 1; }", + { loc: true, raw: true } + ); + let mod = lowerProgram(ast, "twofns"); + assert(mod.functions.length === 2, "two functions"); + verifyModule(mod); +}); + +// --- verifier ------------------------------------------------------------------ + +test("verifier: rejects use that is not dominated by its def", () => { + let fn = new Func("bad", []); + let entry = fn.addBlock(new Block(fn, "entry")); + let a_bb = fn.addBlock(new Block(fn, "a")); + let b_bb = fn.addBlock(new Block(fn, "b")); + entry.sealed = a_bb.sealed = b_bb.sealed = true; + fn.entry = entry; + + let cond = new Inst(fn, "const", [], { kind: "boolean", value: true }); + cond.block = entry; + entry.insts.push(cond); + let cbr = new Inst(fn, "cond_br", [cond], {}); + cbr.block = entry; + entry.insts.push(cbr); + cbr.addTarget(a_bb, []); + cbr.addTarget(b_bb, []); + + let c1 = new Inst(fn, "const", [], { kind: "number", value: 1 }); + c1.block = a_bb; + a_bb.insts.push(c1); + let ra = new Inst(fn, "return", [c1], {}); + ra.block = a_bb; + a_bb.insts.push(ra); + + let bad = new Inst(fn, "strict_eq", [c1, c1], {}); + bad.block = b_bb; + b_bb.insts.push(bad); + let rb = new Inst(fn, "return", [bad], {}); + rb.block = b_bb; + b_bb.insts.push(rb); + + let threw = false; + try { + verifyFunction(fn); + } catch (e) { + threw = /does not dominate/.test((e as Error).message); + } + assert(threw, "expected a dominance violation"); +}); + +test("verifier: rejects unterminated blocks", () => { + let fn = new Func("noterm", []); + let entry = fn.addBlock(new Block(fn, "entry")); + entry.sealed = true; + let c = new Inst(fn, "const", [], { kind: "number", value: 1 }); + c.block = entry; + entry.insts.push(c); + + let threw = false; + try { + verifyFunction(fn); + } catch (e) { + threw = /no terminator/.test((e as Error).message); + } + assert(threw, "expected a no-terminator error"); +}); + +test("verifier: rejects normal edges into catch blocks", () => { + let fn = new Func("badedge", []); + let entry = fn.addBlock(new Block(fn, "entry")); + let catch_bb = fn.addBlock(new Block(fn, "catch")); + catch_bb.isCatch = true; + let exc = catch_bb.addParam("%exception"); + exc.isException = true; + entry.sealed = catch_bb.sealed = true; + fn.entry = entry; + + let br = new Inst(fn, "br", [], {}); + br.block = entry; + entry.insts.push(br); + br.addTarget(catch_bb, []); + + let c = new Inst(fn, "const", [], { kind: "number", value: 0 }); + c.block = catch_bb; + catch_bb.insts.push(c); + let r = new Inst(fn, "return", [c], {}); + r.block = catch_bb; + catch_bb.insts.push(r); + + let threw = false; + try { + verifyFunction(fn); + } catch (e) { + threw = /non-unwind edge into catch/.test((e as Error).message); + } + assert(threw, "expected a catch-edge violation"); +}); + +// --- optimize: allocation sinking ---------------------------------------------- + +function assertNotContains(haystack: string, needle: string): void { + if (haystack.indexOf(needle) !== -1) + throw new Error(`expected output to NOT contain '${needle}'\n---\n${haystack}\n---`); +} + +function lowerAndOptimize(src: string): { fn: Func; printed: string } { + // the module must ride along: flag-off lowering mints + // make_object_shaped for static-key literals, and both shaped sinks + // resolve the shape through the module's shape table + let { module, fn } = lowerOne(src); + optimizeFunction(fn, module); + verifyFunction(fn); + return { fn, printed: printFunction(fn) }; +} + +test("optimize: non-escaping object literal reads fold and the alloc dies", () => { + let { printed } = lowerAndOptimize("function f() { let o = { a: 1, b: 2 }; return o.a + o.b; }"); + assertNotContains(printed, "make_object"); + assertNotContains(printed, "get_prop_atom"); +}); + +test("optimize: duplicate literal keys fold to the last definition", () => { + let { fn, printed } = lowerAndOptimize("function f() { let o = { a: 1, a: 2 }; return o.a; }"); + assertNotContains(printed, "make_object"); + // the surviving return operand should be the const 2 + let ret: Inst | null = null; + fn.forEachInst((i) => { if (i.op === "return") ret = i; }); + assert(ret!.operands[0]!.imms.value === 2, "expected the second definition's value"); +}); + +test("optimize: escaping object literal is untouched", () => { + let { printed } = lowerAndOptimize("function f(g) { let o = { a: 1 }; g(o); return o.a; }"); + assertContainsOp(printed, "make_object_shaped"); + assertContains(printed, 'get_prop_atom'); +}); + +test("optimize: write-only object literal dies with its stores", () => { + let { printed } = lowerAndOptimize("function f(x) { let o = { a: 1 }; o.a = x; return x; }"); + assertNotContains(printed, "make_object"); + assertNotContains(printed, "set_prop_atom"); +}); + +test("optimize: a written key's reads fold flow-sensitively (sinking-P3)", () => { + // the read after the write sees the written value; the store and + // the allocation drain + let { fn, printed } = lowerAndOptimize( + "function f(x) { let o = { a: 1 }; o.a = x; return o.a; }" + ); + assertNotContains(printed, "make_object"); + assertNotContains(printed, "get_prop_atom"); + assertNotContains(printed, "set_prop_atom"); + let ret: Inst | null = null; + fn.forEachInst((i) => { if (i.op === "return") ret = i; }); + assert(ret!.operands[0]!.op === "blockparam", "return should see the written param x"); +}); + +test("optimize: -fno-flow-sink restores the written-key decline", () => { + withPassConfig({ flowSink: false }, () => { + let { printed } = lowerAndOptimize( + "function f(x) { let o = { a: 1 }; o.a = x; return o.a; }" + ); + assertContainsOp(printed, "make_object_shaped"); + assertContains(printed, "get_prop_atom"); + }); +}); + +test("optimize: non-own-key read keeps the object (prototype chain)", () => { + let { printed } = lowerAndOptimize("function f() { let o = { a: 1 }; return o.toString; }"); + assertContainsOp(printed, "make_object_shaped"); +}); + +test("optimize: array literal const-index and length reads fold", () => { + let { printed } = lowerAndOptimize("function f() { let a = [10, 20, 30]; return a[0] + a.length; }"); + assertNotContains(printed, "make_array"); + assertNotContains(printed, "get_prop"); +}); + +test("optimize: array hole reads keep the array", () => { + let { printed } = lowerAndOptimize("function f() { let a = [1, , 3]; return a[1]; }"); + assertContains(printed, "make_array"); +}); + +test("optimize: out-of-range array read keeps the array", () => { + let { printed } = lowerAndOptimize("function f() { let a = [1]; return a[5]; }"); + assertContains(printed, "make_array"); +}); + +test("optimize: computed non-const array read keeps the array", () => { + let { printed } = lowerAndOptimize("function f(i) { let a = [1, 2]; return a[i]; }"); + assertContains(printed, "make_array"); +}); + +test("optimize: array method call keeps the array", () => { + let { printed } = lowerAndOptimize("function f() { let a = [1, 2]; return a.join(','); }"); + assertContains(printed, "make_array"); +}); + +test("optimize: nested literal sinks once the outer one dies", () => { + let { printed } = lowerAndOptimize( + "function f() { let o = { inner: { x: 7 } }; return o.inner.x; }" + ); + assertNotContains(printed, "make_object"); +}); + +test("optimize: object flowing into a block param is an escape", () => { + let { printed } = lowerAndOptimize( + "function f(c) { let o = c ? { a: 1 } : { a: 2 }; return o.a; }" + ); + assertContainsOp(printed, "make_object_shaped"); +}); + +test("optimize: reads inside try (unwind targets) are left alone", () => { + let { printed } = lowerAndOptimize( + "function f() { let o = { a: 1 }; try { return o.a; } catch (e) { return 0; } }" + ); + assertContainsOp(printed, "make_object_shaped"); + assertContains(printed, "get_prop_atom"); +}); + +test("optimize: single-block IIFE inlines and its env scalar-replaces", () => { + let { module, fn } = lowerOne( + "function f(x) { let r = ((a) => a + 1)(x); return r; }" + ); + optimizeFunction(fn, module); + verifyFunction(fn); + let printed = printFunction(fn); + assertNotContains(printed, "make_closure"); + assertNotContains(printed, "call"); + assertContains(printed, "add"); +}); + +test("optimize: escaping closure is not inlined", () => { + let { module, fn } = lowerOne( + "function f(g) { let h = (a) => a + 1; g(h); return h(2); }" + ); + optimizeFunction(fn, module); + verifyFunction(fn); + assertContains(printFunction(fn), "make_closure"); +}); + +test("optimize: same-block env with loads and stores scalar-replaces", () => { + // the arrow captures x, forcing x into an env; after inlining, the + // env ops are all in one block and dissolve + let { module, fn } = lowerOne( + "function f(x) { let get = () => x; return get(); }" + ); + optimizeFunction(fn, module); + verifyFunction(fn); + let printed = printFunction(fn); + assertNotContains(printed, "make_env"); + assertNotContains(printed, "env_load"); + assertNotContains(printed, "call"); +}); + +test("optimize: destructuring swap dissolves to pure SSA", () => { + let r = lowerFunctionNode( + parseFnPreEIR("function f(a, b) { [a, b] = [b, a]; return a - b; }") + ); + optimizeFunction(r.fn, r.module); + verifyFunction(r.fn); + let printed = printFunction(r.fn); + assertNotContains(printed, "make_env"); + assertNotContains(printed, "make_closure"); + assertNotContains(printed, "make_array"); + assertNotContains(printed, "iterator_wrapper_new"); + assertNotContains(printed, "call"); +}); + +test("optimize: iterator walk over a literal folds, short RHS pads undefined", () => { + let r = lowerFunctionNode( + parseFnPreEIR("function f(x) { let [a, b, c] = [x, 2]; return [a, b, c].length && a + b + (c === undefined); }") + ); + optimizeFunction(r.fn, r.module); + verifyFunction(r.fn); + let printed = printFunction(r.fn); + assertNotContains(printed, "iterator_wrapper_new"); + assertNotContains(printed, 'atom="getNextValue"'); +}); + +test("optimize: iterator walk over a non-literal keeps the runtime protocol", () => { + let r = lowerFunctionNode( + parseFnPreEIR("function f(xs) { let [a, b] = xs; return a + b; }") + ); + optimizeFunction(r.fn, r.module); + verifyFunction(r.fn); + assertContains(printFunction(r.fn), 'name="iterator_wrapper_new"'); +}); + +test("optimize: rest pattern (getRest) keeps the runtime protocol", () => { + let r = lowerFunctionNode( + parseFnPreEIR("function f(x, y) { let [a, ...rest] = [x, y, 3]; return a + rest.length; }") + ); + optimizeFunction(r.fn, r.module); + verifyFunction(r.fn); + assertContains(printFunction(r.fn), 'name="iterator_wrapper_new"'); +}); + +test("optimize: array with another use keeps the iterator walk", () => { + let r = lowerFunctionNode( + parseFnPreEIR("function f(x, y) { let arr = [x, y]; let [a] = arr; return a + arr.length; }") + ); + optimizeFunction(r.fn, r.module); + verifyFunction(r.fn); + assertContains(printFunction(r.fn), 'name="iterator_wrapper_new"'); +}); + +test("optimize: env read from a later block is left alone", () => { + // the loop body reads the env across blocks: not same-block, no sink + let { module, fn } = lowerOne( + "function f(x, n) { let get = () => x; while (n) { n = n - get(); } return n; }" + ); + optimizeFunction(fn, module); + verifyFunction(fn); +}); + +test("optimize: DCE removes unused pure chains but keeps effects", () => { + let { printed } = lowerAndOptimize( + "function f(x) { let unused = { a: 1 }; let kept = x.y; return 5; }" + ); + assertNotContains(printed, "make_object"); + // x.y may have observable effects (getter) and must survive + assertContains(printed, "get_prop_atom"); +}); + +// --- the typed low tier ------------------------------------------------ + +function assertVerifyFails(fn: Func, needle: string): void { + try { + verifyFunction(fn); + } catch (err) { + const msg = (err as Error).message; + if (msg.indexOf(needle) === -1) + throw new Error(`verifier failed, but with '${msg}' (wanted '${needle}')`); + return; + } + throw new Error(`verifier accepted an ill-typed function (wanted '${needle}')`); +} + +test("lowtier: printer shows typed defs; untyped stay bare", () => { + const printed = printFunction(buildLowTierAdd("probe")); + assertContains(printed, ': i1 = has_tag'); + assertContains(printed, 'tag="number"'); + assertContains(printed, ": f64 = unbox_f64"); + assertContains(printed, ": f64 = f64_add"); + assertNotContains(printed, ": any ="); // "any" defs print bare + // box_f64 produces a boxed value again: no type annotation + const boxline = printed.split("\n").filter((l) => l.indexOf("box_f64") !== -1 && l.indexOf("unbox") === -1)[0]!; + assert(boxline.indexOf(": f64") === -1 && boxline.indexOf(": i1") === -1, "box_f64 def must be untyped"); +}); + +test("lowtier: the parameterized diamond covers sub/mul/div", () => { + for (const [f64op, generic] of [["f64_sub", "sub"], ["f64_mul", "mul"], ["f64_div", "div"]] as const) { + const fn = buildArithDiamond("probe_" + generic, f64op, generic); + verifyFunction(fn); + const printed = printFunction(fn); + assertContains(printed, ": f64 = " + f64op); + assertContains(printed, generic + " "); + } +}); + +test("lowtier: f64_lt prints as i1 and feeds cond_br", () => { + const printed = printFunction(buildLowTierLt("probe")); + assertContains(printed, ": i1 = f64_lt"); + assertContains(printed, ": f64 = unbox_f64"); +}); + +test("lowtier: verifier accepts the guarded diamonds", () => { + verifyFunction(buildLowTierAdd("ok_add")); // builders verify internally too + verifyFunction(buildLowTierLt("ok_lt")); +}); + +test("lowtier: verifier rejects f64 flowing into a generic op", () => { + const fb = new FunctionBuilder("bad", ["%env", "%this", "a"]); + const a = fb.readVariable("a", fb.cur); + const ua = fb.emit("unbox_f64", [a], {}); + fb.ret(fb.emit("add", [ua, a], {})); + assertVerifyFails(fb.finish(), "may not be f64"); +}); + +test("lowtier: verifier rejects a boxed value in an f64 operand slot", () => { + const fb = new FunctionBuilder("bad", ["%env", "%this", "a"]); + const a = fb.readVariable("a", fb.cur); + const sum = fb.emit("f64_add", [a, a], {}); + fb.ret(fb.emit("box_f64", [sum], {})); + assertVerifyFails(fb.finish(), "wants f64, got any"); +}); + +test("lowtier: verifier rejects i1 where a boxed value is expected", () => { + const fb = new FunctionBuilder("bad", ["%env", "%this", "a"]); + const a = fb.readVariable("a", fb.cur); + const t = fb.emit("has_tag", [a], { tag: "number" }); + fb.ret(t); + assertVerifyFails(fb.finish(), "may not be i1"); +}); + +test("lowtier: verifier rejects an i1 operand to an f64-typed op", () => { + const fb = new FunctionBuilder("bad", ["%env", "%this", "a"]); + const a = fb.readVariable("a", fb.cur); + const t = fb.emit("has_tag", [a], { tag: "number" }); + fb.ret(fb.emit("box_f64", [t], {})); + assertVerifyFails(fb.finish(), "wants f64, got i1"); +}); + +test("lowtier: verifier rejects raw f64/i1 block arguments", () => { + const fb = new FunctionBuilder("bad", ["%env", "%this", "a"]); + const a = fb.readVariable("a", fb.cur); + const ua = fb.emit("unbox_f64", [a], {}); + const join = fb.newBlock("join"); + const jp = join.addParam("jp"); + fb.br(join, [ua]); + fb.sealBlock(join); + fb.setInsertPoint(join); + fb.ret(jp); + assertVerifyFails(fb.finish(), "block arguments must be boxed"); +}); + +test("lowtier: cond_br accepts i1 and legacy any conditions, rejects f64", () => { + const fb = new FunctionBuilder("bad", ["%env", "%this", "a"]); + const a = fb.readVariable("a", fb.cur); + const ua = fb.emit("unbox_f64", [a], {}); + const t = fb.newBlock("t"); + const f = fb.newBlock("f"); + fb.condBr(ua, t, [], f, []); + fb.sealBlock(t); + fb.sealBlock(f); + fb.setInsertPoint(t); + fb.ret(fb.constUndefined()); + fb.setInsertPoint(f); + fb.ret(fb.constUndefined()); + assertVerifyFails(fb.finish(), "cond_br condition may not be f64"); +}); + +test("lowtier: DCE removes dead pure low-tier chains", () => { + const fb = new FunctionBuilder("deadchain", ["%env", "%this", "a"]); + const a = fb.readVariable("a", fb.cur); + const ua = fb.emit("unbox_f64", [a], {}); + const s = fb.emit("f64_add", [ua, ua], {}); + fb.emit("box_f64", [s], {}); // dead: result unused (GC effect is removable) + fb.ret(a); + const fn = fb.finish(); + verifyFunction(fn); + const module = new Module("m"); + module.functions.push(fn); + optimizeFunction(fn, module); + verifyFunction(fn); + const printed = printFunction(fn); + assertNotContains(printed, "f64_add"); + assertNotContains(printed, "box_f64"); + assertNotContains(printed, "unbox_f64"); +}); + +// --- oracle-guided guarded arithmetic ------------------------------------ + +// a hand-built TypeOracle: types Identifier nodes by name, everything else +// (and unknown names) is top. The TypeOracle interface from Chunk G is +// all lowering may consume, so this is a faithful stand-in for maam. +function stubOracle(types: Record): TypeOracle { + return { + typeOfNode: (n) => { + const id = n as { type?: string; name?: string }; + const tags = id.type === "Identifier" && id.name !== undefined ? types[id.name] : undefined; + return tags ? { tags: new Set(tags) } : { tags: "top" }; + }, + closedWorld: () => false, + describe: () => "stub", + }; +} + +function lowerWithOracle(src: string, oracle: TypeOracle | null) { + let r = lowerFunctionNode(parseFn(src), undefined, oracle); + verifyModule(r.module); // (g) every lowered output must verify + return { printed: printFunction(r.fn), diamonds: r.diamonds }; +} + +const DIAMOND_MARKS = ["has_tag", "unbox_f64", "box_f64", "num_join"]; + +test("typed-arith: {number}x{number} + emits the guarded diamond", () => { + const { printed, diamonds } = lowerWithOracle( + "function f(x, y) { return x + y; }", + stubOracle({ x: ["number"], y: ["number"] }) + ); + assert(diamonds === 1, `diamonds=${diamonds}`); + for (const m of DIAMOND_MARKS) assertContains(printed, m); + assertContains(printed, 'tag="number"'); + assertContains(printed, ": f64 = f64_add"); + assertContains(printed, ": f64 = unbox_f64"); + assertContains(printed, ": i1 = has_tag"); +}); + +test("typed-arith: null oracle lowers exactly as before (no diamond)", () => { + const { printed, diamonds } = lowerWithOracle("function f(x, y) { return x + y; }", null); + assert(diamonds === 0, `diamonds=${diamonds}`); + for (const m of DIAMOND_MARKS) assertNotContains(printed, m); + assertContains(printed, "add "); +}); + +test("typed-arith: string operands take no diamond", () => { + const { printed, diamonds } = lowerWithOracle( + "function f(x, y) { return x + y; }", + stubOracle({ x: ["string"], y: ["string"] }) + ); + assert(diamonds === 0, `diamonds=${diamonds}`); + assertNotContains(printed, "has_tag"); +}); + +test("typed-arith: mixed, top, and widened number|undefined take no diamond", () => { + for (const types of [ + { x: ["number"] as TypeTag[], y: ["string"] as TypeTag[] }, // mixed + { x: ["number"] as TypeTag[], y: undefined }, // top + { x: ["number", "undefined"] as TypeTag[], y: ["number"] as TypeTag[] }, // widened + ]) { + const { printed, diamonds } = lowerWithOracle( + "function f(x, y) { return x + y; }", + stubOracle(types) + ); + assert(diamonds === 0, `diamonds=${diamonds} for ${JSON.stringify(types)}`); + assertNotContains(printed, "has_tag"); + } +}); + +test("typed-arith: numeric literals type directly — `x + 1` diamonds", () => { + const { printed, diamonds } = lowerWithOracle( + "function f(x) { return x + 1; }", + stubOracle({ x: ["number"] }) + ); + assert(diamonds === 1, `diamonds=${diamonds}`); + assertContains(printed, ": f64 = f64_add"); + // and a negated literal too (parsed as unary minus over a literal) + const neg = lowerWithOracle("function f(x) { return x - -2; }", stubOracle({ x: ["number"] })); + assert(neg.diamonds === 1, `diamonds=${neg.diamonds}`); + assertContains(neg.printed, ": f64 = f64_sub"); +}); + +test("typed-arith: literals alone do not diamond without an oracle", () => { + const { printed, diamonds } = lowerWithOracle("function f() { return 1 + 2; }", null); + assert(diamonds === 0, `diamonds=${diamonds}`); + assertNotContains(printed, "has_tag"); +}); + +test("typed-arith: `<` diamonds through boolean-constant join edges", () => { + const { printed, diamonds } = lowerWithOracle( + "function f(x, y) { return x < y; }", + stubOracle({ x: ["number"], y: ["number"] }) + ); + assert(diamonds === 1, `diamonds=${diamonds}`); + assertContains(printed, ": i1 = f64_lt"); + assertContains(printed, "num_lt_true"); + assertContains(printed, "num_lt_false"); + // the i1 never reaches the join: its edges carry boolean constants + assertContains(printed, 'kind="boolean", value=true'); + assertContains(printed, 'kind="boolean", value=false'); + assertNotContains(printed, "= box_f64"); // no f64 result to box for `<` (unbox_f64 remains) +}); + +test("typed-arith: mul/div diamonds carry their ops", () => { + for (const [src, op] of [ + ["function f(x, y) { return x * y; }", "f64_mul"], + ["function f(x, y) { return x / y; }", "f64_div"], + ] as const) { + const { printed, diamonds } = lowerWithOracle(src, stubOracle({ x: ["number"], y: ["number"] })); + assert(diamonds === 1, `diamonds=${diamonds}`); + assertContains(printed, ": f64 = " + op); + } +}); + +// --- guard-region merging + raw f64 joins ------------------------------ + +// like the real maam oracle, this types the named identifiers as +// {number} AND any arithmetic expression whose operands are typed — +// hypot2's `a*a + b*b` is three diamonds only because the outer add's +// BinaryExpression operands type as {number} too +function numericStubOracle(names: string[]): TypeOracle { + const numeric = (n: unknown): boolean => { + const node = n as { + type?: string; + name?: string; + operator?: string; + value?: unknown; + left?: unknown; + right?: unknown; + }; + if (node.type === "Identifier") return names.indexOf(node.name!) !== -1; + if (node.type === "Literal") return typeof node.value === "number"; + if ( + node.type === "BinaryExpression" && + (node.operator === "+" || node.operator === "-" || node.operator === "*" || node.operator === "/") + ) + return numeric(node.left) && numeric(node.right); + return false; + }; + return { + typeOfNode: (n) => (numeric(n) ? { tags: new Set(["number"]) } : { tags: "top" }), + closedWorld: () => false, + describe: () => "numeric-stub", + }; +} + +function lowerOptWithOracle(src: string, oracle: TypeOracle | null): { fn: Func; printed: string } { + let r = lowerFunctionNode(parseFn(src), undefined, oracle); + verifyModule(r.module); + optimizeFunction(r.fn, r.module); + verifyFunction(r.fn); + return { fn: r.fn, printed: printFunction(r.fn) }; +} + +function countOps(fn: Func, op: string): number { + let n = 0; + fn.forEachInst((i) => { + if (i.op === op) n++; + }); + return n; +} + +function guardFalseTargets(fn: Func): Set { + const targets = new Set(); + fn.forEachInst((i) => { + if (i.op === "cond_br" && i.operands[0]!.op === "has_tag") targets.add(i.targets![1]!.block); + }); + return targets; +} + +test("guard-fold: x * x re-tests x only once", () => { + const { fn } = lowerOptWithOracle("function f(x) { return x * x; }", numericStubOracle(["x"])); + assert(countOps(fn, "has_tag") === 1, `has_tag = ${countOps(fn, "has_tag")}`); +}); + +test("guard-merge: hypot2 becomes one guard region with one slow path", () => { + // as lowered this is three diamonds / six has_tags (see the guarded-arithmetic + // dump); merged: one has_tag per distinct value, one slow path + const { fn } = lowerOptWithOracle( + "function hypot2(a, b) { return a * a + b * b; }", + numericStubOracle(["a", "b"]) + ); + assert(countOps(fn, "has_tag") === 2, `has_tag = ${countOps(fn, "has_tag")}`); + const ft = guardFalseTargets(fn); + assert(ft.size === 1, `guard-failure targets = ${ft.size}`); + // the generic muls survive on the (single) slow path; the slow add + // is lattice-lowered afterwards (mul results are proven numbers — + // cleanup.ts), so the ToNumber/throw behavior the slow path owes is + // exactly the muls' + assert(countOps(fn, "mul") === 2, "generic muls must survive"); + assert(countOps(fn, "add") === 0, `slow add lowers to f64, saw ${countOps(fn, "add")}`); +}); + +test("guard-merge: merged fast region is unboxed end-to-end, boxing once", () => { + const { fn, printed } = lowerOptWithOracle( + "function hypot2(a, b) { return a * a + b * b; }", + numericStubOracle(["a", "b"]) + ); + // one box at the region exit, one more where cleanup.ts lowers the + // slow path's add over the (proven-number) mul results; the region + // INPUTS unbox on the fast side, the mul results on the slow side + assert(countOps(fn, "box_f64") === 2, `box_f64 = ${countOps(fn, "box_f64")}`); + assert(countOps(fn, "unbox_f64") === 6, `unbox_f64 = ${countOps(fn, "unbox_f64")}`); + // intermediate joins carry raw f64 params (the optimizer-scoped lift + // of the P2 boxed-edges rule), all marked for the verifier + let rawParams = 0; + fn.forEachInst((i) => { + if (i.op === "blockparam" && i.type === "f64") { + assert(i.rawJoin, "f64 param must carry the rawJoin marker"); + rawParams++; + } + }); + assert(rawParams >= 2, `expected f64 join params, got ${rawParams}`); + assertContains(printed, ": f64):"); // an intermediate join's param list +}); + +test("guard-merge: statement chains merge across pure prefixes (bench kernel)", () => { + // i*i, s+_, i/2 (const-operand diamond), -, i+1, s+i: six diamonds, + // two distinct guarded values, const guards fold, one slow path + const { fn } = lowerOptWithOracle( + "function k(s, i) { s = s + i * i - i / 2; i = i + 1; return s + i; }", + numericStubOracle(["s", "i"]) + ); + assert(countOps(fn, "has_tag") === 2, `has_tag = ${countOps(fn, "has_tag")}`); + const ft = guardFalseTargets(fn); + assert(ft.size === 1, `guard-failure targets = ${ft.size}`); + assert(countOps(fn, "box_f64") === 1, `box_f64 = ${countOps(fn, "box_f64")}`); +}); + +test("guard-merge: a non-dominating guard is neither folded nor merged", () => { + // D1 lives in the then-branch: its guards do NOT dominate the second + // x+y after the if-join, so nothing may fold or merge + const { fn } = lowerOptWithOracle( + "function f(c, x, y) { var t = 0; if (c) { t = x + y; } var w = x + y; return t + w; }", + numericStubOracle(["x", "y"]) + ); + assert(countOps(fn, "has_tag") === 4, `has_tag = ${countOps(fn, "has_tag")}`); + const ft = guardFalseTargets(fn); + assert(ft.size === 2, `guard-failure targets = ${ft.size}`); + // both regions still rejoin boxed: no raw params anywhere + let rawParams = 0; + fn.forEachInst((i) => { + if (i.op === "blockparam" && i.type === "f64") rawParams++; + }); + assert(rawParams === 0, `expected no f64 params, got ${rawParams}`); + assert(countOps(fn, "box_f64") === 2, `box_f64 = ${countOps(fn, "box_f64")}`); +}); + +test("guard-merge: `<` diamonds still verify and keep their shape through opt", () => { + const { fn } = lowerOptWithOracle( + "function f(x, y) { return x < y; }", + stubOracle({ x: ["number"], y: ["number"] }) + ); + assert(countOps(fn, "f64_lt") === 1, "lt fast path survives"); + assert(countOps(fn, "lt") === 1, "lt slow path survives"); +}); + +// hand-build one guarded diamond: head cond_br(has_tag v) -> fast|slow, +// fast unbox/f64_mul/box, slow mul(sl, sr), join(param). Returns the +// pieces the attacks need to vary. +function buildDiamond( + fb: FunctionBuilder, + v: Inst, + slowL: Inst, + slowR: Inst, + name: string +): { join: Block; param: Inst; slowOp: Inst } { + const fast = fb.newBlock(name + "_fast"); + const slow = fb.newBlock(name + "_slow"); + const join = fb.newBlock(name + "_join"); + const param = join.addParam(name + "_p"); + const t = fb.emit("has_tag", [v], { tag: "number" }); + fb.condBr(t, fast, [], slow, []); + fb.sealBlock(fast); + fb.sealBlock(slow); + fb.setInsertPoint(fast); + const u = fb.emit("unbox_f64", [v], {}); + fb.br(join, [fb.emit("box_f64", [fb.emit("f64_mul", [u, u], {})], {})]); + fb.setInsertPoint(slow); + const slowOp = fb.emit("mul", [slowL, slowR], {}); + fb.br(join, [slowOp]); + fb.sealBlock(join); + fb.setInsertPoint(join); + return { join: join, param: param, slowOp: slowOp }; +} + +test("guard-merge: a foreign edge into region1's join refuses the merge (attack A)", () => { + // entry picks region1 or a FOREIGN edge handing j1 the unrelated + // value c. Merging would substitute region2's slow operands with + // region1's slow values — wrong on the foreign path. Must refuse. + const fb = new FunctionBuilder("attack_a", ["%env", "%this", "a", "c", "d"]); + const a = fb.readVariable("a", fb.cur); + const c = fb.readVariable("c", fb.cur); + const d = fb.readVariable("d", fb.cur); + const head1 = fb.newBlock("head1"); + const jfor = fb.newBlock("jfor"); + const td = fb.emit("has_tag", [d], { tag: "number" }); + fb.condBr(td, head1, [], jfor, []); + fb.sealBlock(head1); + fb.sealBlock(jfor); + fb.setInsertPoint(head1); + const fast1 = fb.newBlock("fast1"); + const slow1 = fb.newBlock("slow1"); + const j1 = fb.newBlock("j1"); + const p = j1.addParam("p"); + const t1 = fb.emit("has_tag", [a], { tag: "number" }); + fb.condBr(t1, fast1, [], slow1, []); + fb.sealBlock(fast1); + fb.sealBlock(slow1); + fb.setInsertPoint(fast1); + const ua = fb.emit("unbox_f64", [a], {}); + fb.br(j1, [fb.emit("box_f64", [fb.emit("f64_mul", [ua, ua], {})], {})]); + fb.setInsertPoint(slow1); + const m = fb.emit("mul", [a, a], {}); + fb.br(j1, [m]); + // the foreign edge, bypassing region1 entirely + fb.setInsertPoint(jfor); + fb.br(j1, [c]); + fb.sealBlock(j1); + fb.setInsertPoint(j1); + const r2 = buildDiamond(fb, p, p, p, "r2"); + fb.ret(r2.param); + const fn = fb.finish(); + verifyFunction(fn); + const stats = optimizeFunction(fn); + verifyFunction(fn); + assert(stats.regions_merged === 0, `merge must be refused, got ${stats.regions_merged}`); + assert(r2.slowOp.operands[0] === p && r2.slowOp.operands[1] === p, "slow operands untouched"); +}); + +test("guard-merge: a non-twin slow arm refuses the merge (attack F)", () => { + // region2's fast arm computes p*p but its slow arm computes + // mul(p, e). Pre-merge the region1-slow route passes region2's + // guard (mul results are numbers) and takes the FAST arm; the merge + // would reroute it through the non-twin slow arm. Must refuse. + const fb = new FunctionBuilder("attack_f", ["%env", "%this", "a", "e"]); + const a = fb.readVariable("a", fb.cur); + const e = fb.readVariable("e", fb.cur); + const r1 = buildDiamond(fb, a, a, a, "r1"); + const r2 = buildDiamond(fb, r1.param, r1.param, e, "r2"); // slow: mul(p, e) — NOT the twin + fb.ret(r2.param); + const fn = fb.finish(); + verifyFunction(fn); + const stats = optimizeFunction(fn); + verifyFunction(fn); + assert(stats.regions_merged === 0, `merge must be refused, got ${stats.regions_merged}`); +}); + +test("guard-merge: a non-twin REGION1 slow arm refuses the merge (attack G)", () => { + // the mirror of attack F: region1's fast arm computes a*a but its + // slow arm computes mul(a, b) with BOTH operands guard-proven (so + // the re-execution purity check alone would pass); region2 is an + // honest twin on an unrelated c. Post-merge, a c-guard failure + // after region1's fast arm would reroute through region1's non-twin + // slow arm: (a*b)^2 instead of (a*a)^2. Must refuse. + const fb = new FunctionBuilder("attack_g", ["%env", "%this", "a", "b", "c"]); + const a = fb.readVariable("a", fb.cur); + const b = fb.readVariable("b", fb.cur); + const c = fb.readVariable("c", fb.cur); + const g2 = fb.newBlock("g2"); + const fast1 = fb.newBlock("fast1"); + const slow1 = fb.newBlock("slow1"); + const j1 = fb.newBlock("j1"); + const p = j1.addParam("p"); + const t1 = fb.emit("has_tag", [a], { tag: "number" }); + fb.condBr(t1, g2, [], slow1, []); + fb.sealBlock(g2); + fb.setInsertPoint(g2); + const t1b = fb.emit("has_tag", [b], { tag: "number" }); + fb.condBr(t1b, fast1, [], slow1, []); + fb.sealBlock(fast1); + fb.sealBlock(slow1); + fb.setInsertPoint(fast1); + const ua = fb.emit("unbox_f64", [a], {}); + fb.br(j1, [fb.emit("box_f64", [fb.emit("f64_mul", [ua, ua], {})], {})]); // a*a + fb.setInsertPoint(slow1); + const m = fb.emit("mul", [a, b], {}); // NOT the twin; operands both guard-proven + fb.br(j1, [m]); + fb.sealBlock(j1); + fb.setInsertPoint(j1); + // region2: guard the unrelated c, both arms honestly compute p*p + const fast2 = fb.newBlock("fast2"); + const slow2 = fb.newBlock("slow2"); + const j2 = fb.newBlock("j2"); + const q = j2.addParam("q"); + const t2 = fb.emit("has_tag", [c], { tag: "number" }); + fb.condBr(t2, fast2, [], slow2, []); + fb.sealBlock(fast2); + fb.sealBlock(slow2); + fb.setInsertPoint(fast2); + const up = fb.emit("unbox_f64", [p], {}); + fb.br(j2, [fb.emit("box_f64", [fb.emit("f64_mul", [up, up], {})], {})]); + fb.setInsertPoint(slow2); + const n = fb.emit("mul", [p, p], {}); + fb.br(j2, [n]); + fb.sealBlock(j2); + fb.setInsertPoint(j2); + fb.ret(q); + const fn = fb.finish(); + verifyFunction(fn); + const stats = optimizeFunction(fn); + verifyFunction(fn); + assert(stats.regions_merged === 0, `merge must be refused, got ${stats.regions_merged}`); + assert(n.operands[0] === p && n.operands[1] === p, "slow operands untouched"); +}); + +// attack-H family: region1 an honest twin on `a`; region2 guards its +// param p and multiplies p by a const materialized SEPARATELY in each +// arm. With corresponding consts the merge must fire; with +0 vs -0 it +// must refuse (sign of zero is observable via 1/x). +function buildConstPairShape( + fastConst: number, + slowConst: number +): { fn: Func; stats: OptStats } { + const fb = new FunctionBuilder("constpair", ["%env", "%this", "a"]); + const a = fb.readVariable("a", fb.cur); + const r1 = buildDiamond(fb, a, a, a, "r1"); + const p = r1.param; + const fast2 = fb.newBlock("fast2"); + const slow2 = fb.newBlock("slow2"); + const j2 = fb.newBlock("j2"); + const q = j2.addParam("q"); + const t2 = fb.emit("has_tag", [p], { tag: "number" }); + fb.condBr(t2, fast2, [], slow2, []); + fb.sealBlock(fast2); + fb.sealBlock(slow2); + fb.setInsertPoint(fast2); + const cf = fb.emit("const", [], { kind: "number", value: fastConst }); + const uc = fb.emit("unbox_f64", [cf], {}); + const up = fb.emit("unbox_f64", [p], {}); + fb.br(j2, [fb.emit("box_f64", [fb.emit("f64_mul", [uc, up], {})], {})]); + fb.setInsertPoint(slow2); + const cs = fb.emit("const", [], { kind: "number", value: slowConst }); + fb.br(j2, [fb.emit("mul", [cs, p], {})]); + fb.sealBlock(j2); + fb.setInsertPoint(j2); + fb.ret(q); + const fn = fb.finish(); + verifyFunction(fn); + const stats = optimizeFunction(fn); + verifyFunction(fn); + return { fn: fn, stats: stats }; +} + +test("guard-merge: const +0 does not correspond to const -0 (attack H)", () => { + // === would conflate the zeros; the rerouted slow path would flip + // the sign of zero (1/x: Infinity vs -Infinity). Must refuse. + const { stats } = buildConstPairShape(0, -0); + assert(stats.regions_merged === 0, `merge must be refused, got ${stats.regions_merged}`); +}); + +test("guard-merge: distinct NaN consts correspond (one JS NaN)", () => { + // the flip side of Object.is: two const-NaN instructions denote the + // same value on every path, so the honest twin merges + const { stats } = buildConstPairShape(NaN, NaN); + assert(stats.regions_merged === 1, `expected the merge, got ${stats.regions_merged}`); +}); + +test("guard-merge: the twin shape it refuses in attack F merges when honest", () => { + // identical CFG to attack F but with the real generic twin + // (slow: mul(p, p)) — the merge must fire. Guards the twin check + // against being accidentally over-strict. + const fb = new FunctionBuilder("twin_ok", ["%env", "%this", "a"]); + const a = fb.readVariable("a", fb.cur); + const r1 = buildDiamond(fb, a, a, a, "r1"); + const r2 = buildDiamond(fb, r1.param, r1.param, r1.param, "r2"); + fb.ret(r2.param); + const fn = fb.finish(); + verifyFunction(fn); + const stats = optimizeFunction(fn); + verifyFunction(fn); + assert(stats.regions_merged === 1, `expected the merge, got ${stats.regions_merged}`); +}); + +test("rawJoin: a fully-proven loop-carried param converts to f64", () => { + // loop header param fed box_f64 on BOTH the entry and back edges: + // structurally qualified (f64-rooted), converts, and stays sound — + // the dedicated test for the loop-carried conversion path. + const fb = new FunctionBuilder("loopraw", ["%env", "%this", "a"]); + const a = fb.readVariable("a", fb.cur); + const ua = fb.emit("unbox_f64", [a], {}); + const ba = fb.emit("box_f64", [ua], {}); + const header = fb.newBlock("H"); + const hp = header.addParam("s"); + const body = fb.newBlock("body"); + const out = fb.newBlock("out"); + fb.br(header, [ba]); + fb.setInsertPoint(header); + const u = fb.emit("unbox_f64", [hp], {}); + const s = fb.emit("f64_add", [u, u], {}); + const bs = fb.emit("box_f64", [s], {}); + const lt = fb.emit("f64_lt", [s, s], {}); + fb.condBr(lt, body, [], out, []); + fb.sealBlock(body); + fb.setInsertPoint(body); + fb.br(header, [bs]); + fb.sealBlock(header); + fb.sealBlock(out); + fb.setInsertPoint(out); + fb.ret(fb.emit("box_f64", [s], {})); + const fn = fb.finish(); + verifyFunction(fn); + const stats = optimizeFunction(fn); + verifyFunction(fn); + assert(stats.raw_join_params === 1, `raw_join_params = ${stats.raw_join_params}`); + assert(hp.type === "f64" && hp.rawJoin, "loop param must be a marked f64 phi"); + assert(countOps(fn, "unbox_f64") === 1, "the loop-carried unbox collapses"); +}); + +test("verifier: rawJoin marker admits f64 edge args into f64 params", () => { + const fb = new FunctionBuilder("rawjoin", ["%env", "%this", "a"]); + const a = fb.readVariable("a", fb.cur); + const ua = fb.emit("unbox_f64", [a], {}); + const join = fb.newBlock("join"); + const jp = join.addParam("jp"); + jp.type = "f64"; + jp.rawJoin = true; + fb.br(join, [ua]); + fb.sealBlock(join); + fb.setInsertPoint(join); + fb.ret(fb.emit("box_f64", [jp], {})); + verifyFunction(fb.finish()); // accepted +}); + +test("verifier: an f64 param without the rawJoin marker is rejected", () => { + const fb = new FunctionBuilder("norawjoin", ["%env", "%this", "a"]); + const a = fb.readVariable("a", fb.cur); + const ua = fb.emit("unbox_f64", [a], {}); + const join = fb.newBlock("join"); + const jp = join.addParam("jp"); + jp.type = "f64"; // marker NOT set: the strict P2 rule stays in force + fb.br(join, [ua]); + fb.sealBlock(join); + fb.setInsertPoint(join); + fb.ret(fb.emit("box_f64", [jp], {})); + // the edge-side strict rule fires first: without the marker the raw + // f64 argument itself is rejected + assertVerifyFails(fb.finish(), "must be boxed"); +}); + +test("verifier: a boxed arg into a rawJoin f64 param is rejected", () => { + const fb = new FunctionBuilder("boxedarg", ["%env", "%this", "a"]); + const a = fb.readVariable("a", fb.cur); + const join = fb.newBlock("join"); + const jp = join.addParam("jp"); + jp.type = "f64"; + jp.rawJoin = true; + fb.br(join, [a]); // boxed value into the f64 param + fb.sealBlock(join); + fb.setInsertPoint(join); + fb.ret(fb.emit("box_f64", [jp], {})); + assertVerifyFails(fb.finish(), "f64 param"); +}); + +// --- typed calling convention / function specialization --------------- + +// mirror integrate.ts's ordering: lower, optimize, specialize, re-optimize +function specHarness(src: string, oracle: TypeOracle) { + const analysis = new ScopeAnalysis(); + const info = analysis.analyzeFunction(parseFn(src)); + const module = new Module("m"); + const mod_ctx = { + refs: new Map(), + oracle: oracle, + typed_stats: { diamonds: 0, trusted: 0 }, + }; + const fn = lowerAnalyzedFunction(info, analysis, module, mod_ctx); + verifyModule(module); + optimizeModule(module); + verifyModule(module); + const stats = { specialized: 0, sites: 0, rejected: 0, wrapped: 0, fenced: 0 }; + const changed = specializeModule(module, analysis, oracle, null, mod_ctx, stats); + verifyModule(module); + if (changed) { + optimizeModule(module); + verifyModule(module); + // mirror integrate.ts: wrapper clones take a second pass (their + // loop-carried guard proofs need cleanup's param pruning first) + if (stats.wrapped > 0) { + optimizeModule(module); + verifyModule(module); + } + } + return { module: module, outer: fn, stats: stats }; +} + +// a loop keeps the callee out of the EIR inliner's single-block reach, so +// specialization (not inlining) must claim the call sites +const SPEC_KERNEL = + "function k(n) { var s = 0; var i = 0; while (i < n) { s = s + i; i = i + 1; } return s; }"; + +test("specialize: local closed world clones and rewrites call sites", () => { + const { module, outer, stats } = specHarness( + `function outer() { ${SPEC_KERNEL} var r = k(10) + k(20); return r; }`, + numericStubOracle(["n", "s", "i", "r"]) + ); + assert(stats.specialized === 1, `specialized=${stats.specialized}`); + assert(stats.sites === 2, `sites=${stats.sites}`); + assert(stats.rejected === 0, `rejected=${stats.rejected}`); + const clone = module.functions.find((f) => f.name.indexOf("$typed") !== -1); + assert(clone !== undefined, "clone emitted"); + assert(clone!.sig !== null && clone!.sig.result === "f64", "clone sig is f64-result"); + assert(clone!.sig!.formals.length === 1 && clone!.sig!.formals[0] === "f64", "f64 formal"); + // unguarded, slow-path-free body: no tag checks, no generic arithmetic + assert(countOps(clone!, "has_tag") === 0, "clone carries no guards"); + assert(countOps(clone!, "add") === 0, "clone carries no generic ops"); + assert(countOps(clone!, "f64_add") >= 1, "clone computes raw"); + // every return is the raw f64 (printer shows the typed header) + assertContains(printFunction(clone!), "): f64 {"); + // callers: both sites direct, generic dispatch and dead closure gone + assert(countOps(outer, "call_typed") === 2, `call_typed=${countOps(outer, "call_typed")}`); + assert(countOps(outer, "call") === 0, "no generic calls remain"); + assert(countOps(outer, "make_closure") === 0, "dead closure swept"); +}); + +test("specialize: escaping closures are never trusted, even when the oracle lies", () => { + // three escapes: as a return value, into an object literal, as a call + // argument. The (stub) oracle types everything {number} — a wrong + // oracle must not widen what TRUSTED-specializes; the STRUCTURAL + // escape analysis rejects each one. Since runtime-P2 the escapee + // gets the boundary wrapper instead: a guarded (trust-free) clone + // behind entry has_tag guards — a lying oracle costs speed, never + // behavior. + for (const src of [ + `function outer() { ${SPEC_KERNEL} var r = k(1); return k; }`, + // NB: the object must stay LIVE — a dead `{ m: k }` is sunk by the + // optimizer before specialization runs, and an eliminated escape + // is correctly no escape at all + `function outer() { ${SPEC_KERNEL} var o = { m: k }; var r = k(1); return o; }`, + `function outer(h) { ${SPEC_KERNEL} var r = h(k) + k(1); return r; }`, + ]) { + const { module, stats } = specHarness(src, numericStubOracle(["n", "s", "i", "r"])); + assert(stats.specialized === 0, `specialized=${stats.specialized} for ${src}`); + assert(stats.rejected === 0, `rejected=${stats.rejected} for ${src}`); + assert(stats.wrapped === 1, `wrapped=${stats.wrapped} for ${src}`); + assert( + module.functions.every((f) => f.name.indexOf("$typed") === -1), + "no trusted clone" + ); + } +}); + +test("specialize: the boundary wrapper dispatches an escapee to a guarded clone", () => { + const { module, stats } = specHarness( + `function outer(h) { ${SPEC_KERNEL} var r = k(1); h(k); return r; }`, + numericStubOracle(["n", "s", "i", "r"]) + ); + assert(stats.wrapped === 1, `wrapped=${stats.wrapped}`); + assert(stats.specialized === 0, `specialized=${stats.specialized}`); + const clone = module.functions.find((f) => f.name.indexOf("$wrap") !== -1); + assert(clone !== undefined, "wrapper clone emitted"); + assert(clone!.sig !== null && clone!.sig.result === "any", "clone result stays boxed"); + assert(clone!.sig!.formals.length === 1 && clone!.sig!.formals[0] === "f64", "f64 formal"); + // trust-free payoff: the entry box_f64 proofs fold the formal-rooted + // diamonds STRUCTURALLY — raw arithmetic without consuming a single + // oracle claim as fact + assert(countOps(clone!, "f64_add") >= 1, "clone computes raw"); + assert(countOps(clone!, "has_tag") === 0, "formal-rooted guards fold"); + // the generic entry became the wrapper: guard chain, then either the + // typed fast path or the original body + const generic = module.functions.find((f) => !f.sig && f.name.indexOf(".k") !== -1); + assert(generic !== undefined, "generic k survives (it escapes)"); + const entry = generic!.entry!; + assert(entry.params.length === 3, "entry owns the calling convention"); + assert(entry.insts[0]!.op === "has_tag", "guard chain first"); + assert(entry.insts[1]!.op === "cond_br", "guard chain branches"); + assert(countOps(generic!, "call_typed") === 1, "one dispatch to the clone"); + const dispatch: Inst[] = []; + generic!.forEachInst((i) => { + if (i.op === "call_typed") dispatch.push(i); + }); + assert(dispatch[0]!.imms["fn"] === clone!.name, "dispatch targets the wrapper clone"); +}); + +test("specialize: wrapper declines — no payoff, env capture, frame ops", () => { + // a body with nothing to fold: judged (rejected), no wrapper + const noPayoff = specHarness( + `function outer(h) { function k(a) { return "x"; } var r = k(1); h(k); return r; }`, + numericStubOracle(["a", "r"]) + ); + assert(noPayoff.stats.wrapped === 0, `wrapped=${noPayoff.stats.wrapped}`); + assert(noPayoff.stats.rejected === 1, `rejected=${noPayoff.stats.rejected}`); + // an env-capturing escapee: the clone can't honor the env-free ABI + const cap = specHarness( + `function outer(h, c) { function k(n) { var s = 0; while (s < n) { s = s + c; } return s; } h(k); var r = k(1); return r; }`, + numericStubOracle(["n", "s", "c", "r"]) + ); + assert(cap.stats.wrapped === 0, `wrapped=${cap.stats.wrapped}`); + assert(cap.stats.rejected === 1, `rejected=${cap.stats.rejected}`); + // arguments-object use: statically declined, not even judged + const frame = specHarness( + `function outer(h) { function k(n) { var s = arguments.length; while (s < n) { s = s + 1; } return s; } h(k); var r = k(1); return r; }`, + numericStubOracle(["n", "s", "r"]) + ); + assert(frame.stats.wrapped === 0, `wrapped=${frame.stats.wrapped}`); + assert(frame.stats.rejected === 0, `rejected=${frame.stats.rejected}`); +}); + +test("specialize: -fno-export-wrapper leaves the escapee fully generic", () => { + withPassConfig({ exportWrapper: false }, () => { + const { module, stats } = specHarness( + `function outer(h) { ${SPEC_KERNEL} var r = k(1); h(k); return r; }`, + numericStubOracle(["n", "s", "i", "r"]) + ); + assert(stats.wrapped === 0, `wrapped=${stats.wrapped}`); + assert(stats.specialized === 0, `specialized=${stats.specialized}`); + assert( + module.functions.every((f) => f.sig === null), + "no sig'd clones at all" + ); + }); +}); + +test("specialize: env capture and `this` are structurally rejected post-lowering", () => { + // k reads the enclosing c: its clone must load the env it was never + // given — discarded by the envParam post-check, not by the oracle + const cap = specHarness( + `function outer(c) { function k(n) { var s = 0; while (s < n) { s = s + c; } return s; } var r = k(10); return r; }`, + numericStubOracle(["n", "s", "c", "r"]) + ); + assert(cap.stats.specialized === 0, `specialized=${cap.stats.specialized}`); + assert(cap.stats.rejected === 1, `rejected=${cap.stats.rejected}`); + // `this` use survives the (lying) type gate; the thisParam post-check + // discards the clone + const ths = specHarness( + `function outer() { function k(n) { var s = this.z; while (s < n) { s = s + 1; } return s; } var r = k(10); return r; }`, + numericStubOracle(["n", "s", "r"]) + ); + assert(ths.stats.specialized === 0, `specialized=${ths.stats.specialized}`); + assert(ths.stats.rejected === 1, `rejected=${ths.stats.rejected}`); +}); + +test("specialize: non-numeric profiles and non-value returns disqualify early", () => { + for (const [src, names] of [ + // a bare `return;` — no f64 result to promise + [ + `function outer() { function k(n) { var s = 0; while (s < n) { s = s + 1; } if (s < 0) return; return s; } var r = k(5); return r; }`, + ["n", "s", "r"], + ], + // params not provably {number} + [ + `function outer() { ${SPEC_KERNEL} var r = k(10); return r; }`, + ["s", "i", "r"], // n missing: top + ], + // arguments-object use + [ + `function outer() { function k(n) { var s = arguments.length; while (s < n) { s = s + 1; } return s; } var r = k(5); return r; }`, + ["n", "s", "r"], + ], + ] as [string, string[]][]) { + const { stats } = specHarness(src, numericStubOracle(names)); + assert(stats.specialized === 0, `specialized=${stats.specialized} for ${src}`); + } +}); + +test("specialize: arity-mismatched sites keep the generic path", () => { + // k(10, 99) passes an extra arg: still an enumerated site (correct to + // leave generic), so the clone ships and only the exact-arity site + // rewrites — the closure must SURVIVE for the generic site + const { module, outer, stats } = specHarness( + `function outer() { ${SPEC_KERNEL} var r = k(10) + k(20, 99); return r; }`, + numericStubOracle(["n", "s", "i", "r"]) + ); + assert(stats.specialized === 1, `specialized=${stats.specialized}`); + assert(stats.sites === 1, `sites=${stats.sites}`); + assert(countOps(outer, "call_typed") === 1, "one direct site"); + assert(countOps(outer, "call") === 1, "one generic site survives"); + assert(countOps(outer, "make_closure") === 1, "closure still needed"); + assert(module.functions.some((f) => f.sig !== null), "clone present"); +}); + +test("verifier: an f64 entry param requires a matching sig", () => { + const fb = new FunctionBuilder("sigless", ["%env", "%this", "x"]); + const x = fb.fn.entry!.params[2]!; + x.type = "f64"; + fb.ret(fb.constUndefined()); + assertVerifyFails(fb.finish(), "rawJoin"); + + const fb2 = new FunctionBuilder("sigged", ["%env", "%this", "x"]); + fb2.fn.sig = { formals: ["f64"], result: "any" }; + const x2 = fb2.fn.entry!.params[2]!; + x2.type = "f64"; + fb2.ret(fb2.emit("box_f64", [x2], {})); + verifyFunction(fb2.finish()); +}); + +test("verifier: an f64-result function must return raw f64", () => { + const fb = new FunctionBuilder("f64ret", ["%env", "%this", "x"]); + fb.fn.sig = { formals: ["f64"], result: "f64" }; + fb.fn.entry!.params[2]!.type = "f64"; + fb.ret(fb.constUndefined()); + assertVerifyFails(fb.finish(), "f64-result"); +}); + +test("verifier: call_typed is checked against the callee sig", () => { + const mkCallee = (): Func => { + const fb = new FunctionBuilder("callee$typed", ["%env", "%this", "x"]); + fb.fn.sig = { formals: ["f64"], result: "f64" }; + const x = fb.fn.entry!.params[2]!; + x.type = "f64"; + fb.ret(fb.emit("f64_add", [x, x], {})); + return fb.finish(); + }; + const mkCaller = (argIsRaw: boolean, resultType: string, calleeName: string): Func => { + const fb = new FunctionBuilder("caller", ["%env", "%this", "a"]); + const a = fb.readVariable("a", fb.cur); + const env = fb.constUndefined(); + const arg = argIsRaw ? fb.emit("unbox_f64", [a], {}) : a; + const ct = fb.emit("call_typed", [env, arg], { fn: calleeName }); + ct.type = resultType; + fb.ret(fb.emit("box_f64", [ct], {})); + return fb.finish(); + }; + const assertModuleFails = (m: Module, needle: string): void => { + try { + verifyModule(m); + } catch (err) { + const msg = (err as Error).message; + if (msg.indexOf(needle) === -1) + throw new Error(`verifier failed, but with '${msg}' (wanted '${needle}')`); + return; + } + throw new Error(`verifier accepted a bad call_typed (wanted '${needle}')`); + }; + + // well-typed: passes + const ok = new Module("ok"); + ok.addFunction(mkCallee()); + ok.addFunction(mkCaller(true, "f64", "callee$typed")); + verifyModule(ok); + + // boxed arg into an f64 formal + const bad1 = new Module("bad1"); + bad1.addFunction(mkCallee()); + bad1.addFunction(mkCaller(false, "f64", "callee$typed")); + assertModuleFails(bad1, "wants f64"); + + // stamped result type contradicts the callee sig + const bad2 = new Module("bad2"); + bad2.addFunction(mkCallee()); + const wrongResult = (() => { + const fb = new FunctionBuilder("caller2", ["%env", "%this", "a"]); + const a = fb.readVariable("a", fb.cur); + const arg = fb.emit("unbox_f64", [a], {}); + const ct = fb.emit("call_typed", [fb.constUndefined(), arg], { fn: "callee$typed" }); + // ct.type left "any": lies about the f64 result + fb.ret(ct); + return fb.finish(); + })(); + bad2.addFunction(wrongResult); + assertModuleFails(bad2, "result type"); + + // unknown callee + const bad3 = new Module("bad3"); + bad3.addFunction(mkCaller(true, "f64", "nowhere$typed")); + assertModuleFails(bad3, "unknown function"); +}); + +test("opt: unbox_f64 of a number const folds to f64_const", () => { + const fb = new FunctionBuilder("cfold", ["%env", "%this"]); + const c = fb.constNumber(2); + const u = fb.emit("unbox_f64", [c], {}); + const v = fb.emit("f64_add", [u, u], {}); + fb.ret(fb.emit("box_f64", [v], {})); + const fn = fb.finish(); + verifyFunction(fn); + const s = optimizeFunction(fn); + verifyFunction(fn); + assert(s.unbox_folds === 1, `unbox_folds=${s.unbox_folds}`); + assert(countOps(fn, "f64_const") === 1, "raw const minted"); + assert(countOps(fn, "unbox_f64") === 0, "unbox gone"); +}); + +test("opt: constant boolean edges thread past to_boolean re-tests", () => { + // the `<` diamond's fast arm: after threading, its constant edges + // branch directly and only the slow (generic) edge still re-tests + const r = lowerFunctionNode( + parseFn("function f(x, y) { if (x < y) { return 1; } return 2; }"), + undefined, + numericStubOracle(["x", "y"]) + ); + verifyModule(r.module); + const s = optimizeFunction(r.fn, r.module); + verifyFunction(r.fn); + assert(s.joins_threaded === 2, `joins_threaded=${s.joins_threaded}`); + // the join survives for the slow arm's boxed value, still re-tested + assert(countOps(r.fn, "to_boolean") === 1, "slow-arm re-test survives"); +}); + +// --- oracle: TypeSig -> EirType mapping ----------------------------------------- + +test("oracle: TypeSig constituents map to EirType tags", () => { + const t = typeSigToEirType("num|str"); + assert(t.tags !== "top"); + const tags = t.tags as ReadonlySet; + assert(tags.size === 2 && tags.has("number") && tags.has("string")); + const all = typeSigToEirType("num|str|bool|null|undefined|fn|obj").tags as ReadonlySet; + assert(all.size === 7 && all.has("closure") && all.has("object") && all.has("null")); +}); + +test("oracle: top, never, and missing sigs are all top", () => { + assert(typeSigToEirType("\u22a4").tags === "top"); + assert(typeSigToEirType("never").tags === "top"); + assert(typeSigToEirType(undefined).tags === "top"); +}); + +test("oracle: an unrecognized constituent is top, never a guess", () => { + assert(typeSigToEirType("num|widget").tags === "top"); + assert(typeSigToEirType("bigint").tags === "top"); + assert(typeSigToEirType("").tags === "top"); +}); + +// --- shape-guarded property access ---------------------------- + +test("shape-oracle: TypeSig -> repr (num=f64, non-num unions=boxed, straddles decline)", () => { + assert(typeSigToShapeRepr("num") === "f64"); + assert(typeSigToShapeRepr("str") === "boxed"); + assert(typeSigToShapeRepr("str|bool|undefined|null|obj|fn") === "boxed"); + assert(typeSigToShapeRepr("num|str") === null); + assert(typeSigToShapeRepr("⊤") === null); + assert(typeSigToShapeRepr("never") === null); + assert(typeSigToShapeRepr("num|widget") === null); +}); + +// a stub oracle with receiver-shape facts: types Identifier receivers by +// name; everything else declines as unmapped (the real oracle's fail-soft). +// A receiver may carry one shape (mono) or two (the poly chain). +function stubShapeOracle( + shapes: Record, + types?: Record +): TypeOracle { + const base = stubOracle(types || {}); + return { + ...base, + receiverShapeOfNode: (n) => { + const id = n as { type?: string; name?: string }; + const entry = + id.type === "Identifier" && id.name !== undefined ? shapes[id.name] : undefined; + if (!entry) return { declined: "unmapped" }; + const list = Array.isArray(entry[0]) ? (entry as OracleShapeField[][]) : [entry as OracleShapeField[]]; + return { shapes: list }; + }, + }; +} + +const PXY: OracleShapeField[] = [ + { name: "x", repr: "f64" }, + { name: "y", repr: "f64" }, + { name: "s", repr: "boxed" }, +]; + +test("shapes: exact receiver fact lowers a get to the has_shape diamond", () => { + const { printed } = lowerWithOracle( + "function f(p) { return p.y; }", + stubShapeOracle({ p: PXY }) + ); + assertContains(printed, 'has_shape'); + assertContains(printed, 'shape="x:f64,y:f64,s:boxed"'); + assertContains(printed, 'slot_load'); + assertContains(printed, 'slot=1'); + assertContains(printed, 'repr="f64"'); + assertContains(printed, 'get_prop_atom'); // the slow arm survives + assertContains(printed, "shape_join"); +}); + +test("shapes: no shape query support means today's lowering exactly", () => { + const { printed } = lowerWithOracle( + "function f(p) { return p.y; }", + stubOracle({ p: undefined }) + ); + assertNotContains(printed, "has_shape"); + assertNotContains(printed, "slot_load"); +}); + +test("shapes: a field outside the shape declines (proto/method access)", () => { + const { printed } = lowerWithOracle( + "function f(p) { return p.z; }", + stubShapeOracle({ p: PXY }) + ); + assertNotContains(printed, "has_shape"); + assertContains(printed, 'get_prop_atom'); +}); + +test("shapes: an f64-field store guards has_shape AND has_tag, numbers fast", () => { + const { printed } = lowerWithOracle( + "function f(p, v) { p.x = v; }", + stubShapeOracle({ p: PXY }) + ); + assertContains(printed, "has_shape"); + assertContains(printed, "has_tag"); + assertContains(printed, "slot_store"); + assertContains(printed, "set_prop_atom"); + // f64 field: the tag-true edge is the fast arm + assert( + /cond_br %\d+ -> \^shape_setfast\d+\(\), \^shape_setslow\d+\(\)/.test(printed), + "expected tag-true -> fast for an f64 field" + ); +}); + +test("shapes: a boxed-field store takes non-numbers fast (swapped tag arms)", () => { + const { printed } = lowerWithOracle( + "function f(p, v) { p.s = v; }", + stubShapeOracle({ p: PXY }) + ); + assertContains(printed, 'repr="boxed"'); + // boxed field: the tag-true edge is the SLOW arm + assert( + /cond_br %\d+ -> \^shape_setslow\d+\(\), \^shape_setfast\d+\(\)/.test(printed), + "expected tag-true -> slow for a boxed field" + ); +}); + +test("shapes: -fno-shape-guards disables the diamonds", () => { + withPassConfig({ shapeGuards: false }, () => { + const { printed } = lowerWithOracle( + "function f(p) { return p.y; }", + stubShapeOracle({ p: PXY }) + ); + assertNotContains(printed, "has_shape"); + }); +}); + +// --- 2-way polymorphic guard chains ---------------------------- + +// the second class of the poly pair: same fields x/y at DIFFERENT slots +// (plus its own z), so per-arm slot immediates are observable +const PZXY: OracleShapeField[] = [ + { name: "z", repr: "f64" }, + { name: "x", repr: "f64" }, + { name: "y", repr: "f64" }, +]; + +test("shapes-poly: two exact shapes lower a get to a guard chain, one slow path", () => { + const { printed } = lowerWithOracle( + "function f(p) { return p.y; }", + stubShapeOracle({ p: [PXY, PZXY] }) + ); + const guards = (printed.match(/has_shape/g) || []).length; + assert(guards === 2, `expected 2 chained guards, got ${guards}`); + assertContains(printed, 'shape="x:f64,y:f64,s:boxed"'); + assertContains(printed, 'shape="z:f64,x:f64,y:f64"'); + assertContains(printed, "shape_chk"); // the second guard tests on the first's miss edge + assertContains(printed, "slot=1"); // y in {x,y,s} + assertContains(printed, "slot=2"); // y in {z,x,y} + const slows = (printed.match(/get_prop_atom/g) || []).length; + assert(slows === 1, `the chain shares ONE generic slow path, got ${slows}`); +}); + +test("shapes-poly: a field absent from either shape declines the whole site", () => { + // s lives only in PXY: the PZXY arm would need proto-lookup semantics, + // which only the generic path has (criterion 2 — no near-misses) + const { printed } = lowerWithOracle( + "function f(p) { return p.s; }", + stubShapeOracle({ p: [PXY, PZXY] }) + ); + assertNotContains(printed, "has_shape"); + assertContains(printed, "get_prop_atom"); +}); + +test("shapes-poly: stores chain with a tag split per arm, one generic path", () => { + const { printed } = lowerWithOracle( + "function f(p, v) { p.x = v; }", + stubShapeOracle({ p: [PXY, PZXY] }) + ); + assert((printed.match(/has_shape/g) || []).length === 2, "2 chained guards"); + assert((printed.match(/has_tag/g) || []).length === 2, "a tag split per arm"); + assert((printed.match(/slot_store/g) || []).length === 2, "a typed store per arm"); + assert((printed.match(/set_prop_atom/g) || []).length === 1, "one generic path"); + assertContains(printed, "shape_setchk"); +}); + +test("shapes-poly: mixed reprs orient each arm by its own field repr", () => { + const A: OracleShapeField[] = [{ name: "x", repr: "f64" }]; + const B: OracleShapeField[] = [ + { name: "x", repr: "boxed" }, + { name: "w", repr: "boxed" }, + ]; + const get = lowerWithOracle( + "function f(p) { return p.x; }", + stubShapeOracle({ p: [A, B] }) + ).printed; + // only the f64 arm boxes its raw load + assert((get.match(/box_f64/g) || []).length === 1, "exactly one arm boxes"); + assertContains(get, 'repr="f64"'); + assertContains(get, 'repr="boxed"'); + const set = lowerWithOracle( + "function f(p, v) { p.x = v; }", + stubShapeOracle({ p: [A, B] }) + ).printed; + // one arm takes numbers fast (tag-true -> fast), the other non-numbers + assert( + /cond_br %\d+ -> \^shape_setfast\d+\(\), \^shape_setslow\d+\(\)/.test(set), + "f64 arm: tag-true -> fast" + ); + assert( + /cond_br %\d+ -> \^shape_setslow\d+\(\), \^shape_setfast\d+\(\)/.test(set), + "boxed arm: tag-true -> slow" + ); +}); + +test("shapes-poly: -fno-poly-shape-guards declines 2-shape sites, keeps mono", () => { + withPassConfig({ polyShapeGuards: false }, () => { + const poly = lowerWithOracle( + "function f(p) { return p.y; }", + stubShapeOracle({ p: [PXY, PZXY] }) + ).printed; + assertNotContains(poly, "has_shape"); + const mono = lowerWithOracle( + "function f(p) { return p.y; }", + stubShapeOracle({ p: PXY }) + ).printed; + assertContains(mono, "has_shape"); + }); +}); + +test("shapes-poly: structurally equal shapes reported twice guard once", () => { + const { printed } = lowerWithOracle( + "function f(p) { return p.y; }", + stubShapeOracle({ p: [PXY, PXY] }) + ); + const guards = (printed.match(/has_shape/g) || []).length; + assert(guards === 1, `duplicate shapes must dedupe to a mono diamond, got ${guards}`); +}); + +// --- shapes: verifier rules (hand-built attack IR) ------------------------------- + +function assertThrows(fn: () => void, needle: string): void { + let threw: string | null = null; + try { + fn(); + } catch (e) { + threw = (e as Error).message; + } + assert(threw !== null, `expected a verifier rejection containing '${needle}'`); + assert( + threw!.includes(needle), + `expected rejection containing '${needle}', got: ${threw}` + ); +} + +interface SlotAttackOpts { + guarded?: boolean; // guard the slot op with has_shape (default true) + killInFast?: boolean; // a call between the guard and the slot op + store?: boolean; // slot_store instead of slot_load + storeRaw?: boolean; // unbox the stored value (the typed store form) + tagGuard?: "none" | "true" | "false"; // has_tag fact for the stored value + slot?: number; + repr?: string; + boxedField?: boolean; // shape's x field is boxed (for boxed-store rules) + loadType?: string; // override the slot_load result stamp (attack) + shapeImm?: string; // override the op's shape imm +} + +// head: guard (or an unrelated to_boolean test) -> fast/slow -> join +function buildSlotAttack(o: SlotAttackOpts): { mod: Module; fn: Func } { + const guarded = o.guarded !== false; + const fb = new FunctionBuilder("attack", ["%env", "%this", "p", "v"]); + const p = fb.fn.entry!.params[2]!; + const v = fb.fn.entry!.params[3]!; + const shapeKey = o.boxedField ? "x:boxed,y:f64" : "x:f64,y:f64"; + const opShape = o.shapeImm ?? shapeKey; + const repr = o.repr ?? (o.boxedField ? "boxed" : "f64"); + + const fast = fb.newBlock("fast"); + const slow = fb.newBlock("slow"); + const join = fb.newBlock("join"); + const res = join.addParam("res"); + const cond = guarded + ? fb.emit("has_shape", [p], { shape: shapeKey }) + : fb.emit("to_boolean", [p], {}); + fb.condBr(cond, fast, [], slow, []); + fb.sealBlock(fast); + fb.sealBlock(slow); + + fb.setInsertPoint(fast); + if (o.killInFast) fb.emit("call_runtime", [], { name: "ToString" }); + let stored = v; + if (o.store && o.tagGuard && o.tagGuard !== "none") { + // establish the tag fact: a nested has_tag diamond whose surviving + // arm continues to the store + const tagok = fb.newBlock("tagok"); + const tagbail = fb.newBlock("tagbail"); + const t = fb.emit("has_tag", [stored], { tag: "number" }); + if (o.tagGuard === "true") fb.condBr(t, tagok, [], tagbail, []); + else fb.condBr(t, tagbail, [], tagok, []); + fb.sealBlock(tagok); + fb.sealBlock(tagbail); + fb.setInsertPoint(tagbail); + fb.br(join, [fb.constUndefined()]); + fb.setInsertPoint(tagok); + } + if (o.storeRaw) stored = fb.emit("unbox_f64", [stored], {}); + let fastv: Inst; + if (o.store) { + fastv = fb.emit("slot_store", [p, stored], { + shape: opShape, + slot: o.slot ?? 0, + repr: repr, + }); + fb.br(join, [fb.constUndefined()]); + } else { + fastv = fb.emit("slot_load", [p], { + shape: opShape, + slot: o.slot ?? 0, + repr: repr, + }); + // an f64-repr load produces a raw f64 (stamped by lowering) + // and boxes at the fast exit; loadType overrides for attack IR + fastv.type = o.loadType ?? (repr === "f64" ? "f64" : "any"); + if (fastv.type === "f64") fastv = fb.emit("box_f64", [fastv], {}); + fb.br(join, [fastv]); + } + + fb.setInsertPoint(slow); + const g = fb.emit("get_prop_atom", [p], { atom: "x" }); + fb.br(join, [g]); + + fb.sealBlock(join); + fb.setInsertPoint(join); + fb.ret(res); + + const fn = fb.finish(); + const mod = new Module("attack_mod"); + mod.addFunction(fn); + mod.internShape([ + { name: "x", repr: o.boxedField ? "boxed" : "f64" }, + { name: "y", repr: "f64" }, + ]); + return { mod, fn }; +} + +test("shapes-verify: a guarded slot_load in the guard's true arm verifies", () => { + const { mod } = buildSlotAttack({}); + verifyModule(mod); +}); + +test("shapes-verify: a slot op without a has_shape fact is rejected", () => { + const { mod } = buildSlotAttack({ guarded: false }); + assertThrows(() => verifyModule(mod), "un-killed has_shape fact"); +}); + +test("shapes-verify: a WRITE|CALL between guard and slot op kills the fact", () => { + const { mod } = buildSlotAttack({ killInFast: true }); + assertThrows(() => verifyModule(mod), "un-killed has_shape fact"); +}); + +test("shapes-verify: slot out of bounds / repr mismatch / unknown shape reject", () => { + assertThrows(() => verifyModule(buildSlotAttack({ slot: 2 }).mod), "out of bounds"); + assertThrows(() => verifyModule(buildSlotAttack({ repr: "boxed" }).mod), "shape field repr"); + assertThrows( + () => verifyModule(buildSlotAttack({ shapeImm: "a:boxed" }).mod), + "unknown module shape" + ); +}); + +test("shapes-verify: slot_store repr proofs — typed f64, tagged boxed", () => { + // an f64 store takes a raw f64 — the type system IS the proof; + // no has_tag fact anywhere and it still verifies + verifyModule(buildSlotAttack({ store: true, storeRaw: true }).mod); + // a BOXED value into an f64 slot is a type error, has_tag fact or not + assertThrows( + () => verifyModule(buildSlotAttack({ store: true }).mod), + "raw f64" + ); + assertThrows( + () => verifyModule(buildSlotAttack({ store: true, tagGuard: "true" }).mod), + "raw f64" + ); + // a boxed-repr store still requires the has_tag=false fact + verifyModule(buildSlotAttack({ store: true, boxedField: true, tagGuard: "false" }).mod); + assertThrows( + () => verifyModule(buildSlotAttack({ store: true, boxedField: true, tagGuard: "none" }).mod), + "has_tag" + ); + // the WRONG edge's fact (value proven number, field repr boxed) rejects + assertThrows( + () => verifyModule(buildSlotAttack({ store: true, boxedField: true, tagGuard: "true" }).mod), + "has_tag" + ); + // a raw f64 into a BOXED slot is a type error + assertThrows( + () => + verifyModule( + buildSlotAttack({ store: true, boxedField: true, storeRaw: true }).mod + ), + "boxed value" + ); +}); + +test("shapes-verify: slot_load result stamp must match its repr", () => { + // an f64-repr load left stamped "any" is rejected (the boxed + // form no longer verifies)... + assertThrows( + () => verifyModule(buildSlotAttack({ loadType: "any" }).mod), + "must have type f64" + ); + // ...and a boxed-repr load stamped f64 likewise + assertThrows( + () => verifyModule(buildSlotAttack({ boxedField: true, loadType: "f64" }).mod), + "must have type any" + ); +}); + +// --- shapes: optimizer (merging + fact folding) ---------------------------------- + +function shapeOptStats(): OptStats { + return { + allocs_sunk: 0, + reads_folded: 0, + calls_inlined: 0, + iters_folded: 0, + dead_removed: 0, + guards_folded: 0, + regions_merged: 0, + raw_join_params: 0, + shape_guards_folded: 0, + shape_regions_merged: 0, + shape_numeric_merged: 0, + unbox_folds: 0, + joins_threaded: 0, + shape_allocs_sunk: 0, + shape_guards_sunk: 0, + args_sunk: 0, + flow_allocs_sunk: 0, + allocs_materialized: 0, + consts_folded: 0, + branches_folded: 0, + params_pruned: 0, + typeof_rewrites: 0, + lattice_arith: 0, + slot_loads_cse: 0, + }; +} + +function lowerShapeOpt(src: string): { printed: string; stats: OptStats } { + const r = lowerFunctionNode(parseFn(src), undefined, stubShapeOracle({ p: PXY })); + verifyModule(r.module); + const stats = optimizeFunction(r.fn, r.module); + verifyModule(r.module); + return { printed: printFunction(r.fn), stats }; +} + +test("shapes-opt: consecutive gets on one receiver merge to one guard region", () => { + const { printed, stats } = lowerShapeOpt("function f(p) { return p.x + p.x; }"); + assert(stats.shape_regions_merged === 1, `merged=${stats.shape_regions_merged}`); + assert(stats.shape_guards_folded === 1, `folded=${stats.shape_guards_folded}`); + const guards = (printed.match(/has_shape/g) || []).length; + assert(guards === 1, `expected 1 surviving has_shape, got ${guards}`); + const loads = (printed.match(/slot_load/g) || []).length; + assert(loads === 2, `expected 2 slot_loads, got ${loads}`); +}); + +test("shapes-poly-opt: chains pass the optimizer un-merged and re-verify", () => { + // The region matcher and fact folder are mono-strict by construction: + // a poly chain's first guard has the second CHECK block as its miss + // edge (not a generic slow arm) and its join has three predecessors, + // so both machineries must refuse — everything survives verbatim and + // the module re-verifies. (Chain-aware merging is future measured + // work; kernel wall time is at mono parity without it.) + const r = lowerFunctionNode( + parseFn("function f(p) { return p.x + p.x; }"), + undefined, + stubShapeOracle({ p: [PXY, PZXY] }) + ); + verifyModule(r.module); + const stats = optimizeFunction(r.fn, r.module); + verifyModule(r.module); + const printed = printFunction(r.fn); + assert(stats.shape_regions_merged === 0, `merged=${stats.shape_regions_merged}`); + assert(stats.shape_guards_folded === 0, `folded=${stats.shape_guards_folded}`); + assert(stats.shape_numeric_merged === 0, `het-merged=${stats.shape_numeric_merged}`); + const guards = (printed.match(/has_shape/g) || []).length; + assert(guards === 4, `2 sites x 2 chained guards must survive, got ${guards}`); +}); + +test("shapes-opt: a call between accesses kills the facts and refuses the merge", () => { + const { printed, stats } = lowerShapeOpt( + "function f(p, g) { var a = p.x; g(); return a + p.x; }" + ); + assert(stats.shape_regions_merged === 0, `merged=${stats.shape_regions_merged}`); + assert(stats.shape_guards_folded === 0, `folded=${stats.shape_guards_folded}`); + const guards = (printed.match(/has_shape/g) || []).length; + assert(guards === 2, `expected both has_shape guards to survive, got ${guards}`); +}); + +test("shapes-opt: store diamonds do not match the get-region shape", () => { + const { stats } = lowerShapeOpt("function f(p) { p.x = p.x + 1; return p.x; }"); + // the has_tag split in the store's fast side refuses region matching; + // nothing may merge across a slot_store (it is a WRITE kill) + assert(stats.shape_regions_merged === 0, `merged=${stats.shape_regions_merged}`); +}); + +// hand-built twin-mismatch attack: two adjacent get regions whose slow +// arms LIE (region2's generic get names a different field than its fast +// slot_load) — the merge must refuse on the twin check +function buildTwinAttack(lieAtom: string): { mod: Module; fn: Func; stats: OptStats } { + const fb = new FunctionBuilder("twin", ["%env", "%this", "p"]); + const p = fb.fn.entry!.params[2]!; + const shapeKey = "x:f64,y:f64"; + + const fast1 = fb.newBlock("fast1"); + const slow1 = fb.newBlock("slow1"); + const j1 = fb.newBlock("j1"); + const p1 = j1.addParam("v1"); + const g1 = fb.emit("has_shape", [p], { shape: shapeKey }); + fb.condBr(g1, fast1, [], slow1, []); + fb.sealBlock(fast1); + fb.sealBlock(slow1); + fb.setInsertPoint(fast1); + const l1 = fb.emit("slot_load", [p], { shape: shapeKey, slot: 0, repr: "f64" }); + l1.type = "f64"; + fb.br(j1, [fb.emit("box_f64", [l1], {})]); + fb.setInsertPoint(slow1); + const gp1 = fb.emit("get_prop_atom", [p], { atom: "x" }); + fb.br(j1, [gp1]); + fb.sealBlock(j1); + fb.setInsertPoint(j1); + + const fast2 = fb.newBlock("fast2"); + const slow2 = fb.newBlock("slow2"); + const j2 = fb.newBlock("j2"); + const p2 = j2.addParam("v2"); + const g2 = fb.emit("has_shape", [p], { shape: shapeKey }); + fb.condBr(g2, fast2, [], slow2, []); + fb.sealBlock(fast2); + fb.sealBlock(slow2); + fb.setInsertPoint(fast2); + const l2 = fb.emit("slot_load", [p], { shape: shapeKey, slot: 0, repr: "f64" }); + l2.type = "f64"; + fb.br(j2, [fb.emit("box_f64", [l2], {})]); + fb.setInsertPoint(slow2); + const gp2 = fb.emit("get_prop_atom", [p], { atom: lieAtom }); + fb.br(j2, [gp2]); + fb.sealBlock(j2); + fb.setInsertPoint(j2); + const sum = fb.emit("add", [p1, p2], {}); + fb.ret(sum); + + const fn = fb.finish(); + const mod = new Module("twin_mod"); + mod.addFunction(fn); + mod.internShape([ + { name: "x", repr: "f64" }, + { name: "y", repr: "f64" }, + ]); + const stats = shapeOptStats(); + optimizeShapeRegions(fn, mod, stats); + verifyModule(mod); + return { mod, fn, stats }; +} + +test("shapes-opt: a lying slow twin refuses the merge; the honest one merges", () => { + const lying = buildTwinAttack("y"); + assert(lying.stats.shape_regions_merged === 0, "lying twin must not merge"); + const honest = buildTwinAttack("x"); + assert(honest.stats.shape_regions_merged === 1, "honest twin must merge"); + assert(honest.stats.shape_guards_folded === 1, "post-merge guard must fold"); +}); + +// stale-compare attack: the fact holds at the branch, but the compare was +// computed BEFORE the region that establishes it — folding it to true +// would take the wrong arm when the compare was false at its own site +test("shapes-opt: a stale (earlier-block) has_shape compare never folds", () => { + const fb = new FunctionBuilder("stale", ["%env", "%this", "p"]); + const p = fb.fn.entry!.params[2]!; + const shapeKey = "x:f64,y:f64"; + const t1 = fb.newBlock("t1"); + const out = fb.newBlock("out"); + const a = fb.newBlock("a"); + const bb = fb.newBlock("b"); + const stale = fb.emit("has_shape", [p], { shape: shapeKey }); + const g1 = fb.emit("has_shape", [p], { shape: shapeKey }); + fb.condBr(g1, t1, [], out, []); + fb.sealBlock(t1); + fb.setInsertPoint(t1); + fb.condBr(stale, a, [], bb, []); + fb.sealBlock(a); + fb.sealBlock(bb); + fb.setInsertPoint(a); + fb.br(out, []); + fb.setInsertPoint(bb); + fb.br(out, []); + fb.sealBlock(out); + fb.setInsertPoint(out); + fb.ret(fb.constUndefined()); + + const fn = fb.finish(); + const mod = new Module("stale_mod"); + mod.addFunction(fn); + mod.internShape([ + { name: "x", repr: "f64" }, + { name: "y", repr: "f64" }, + ]); + verifyModule(mod); + const stats = shapeOptStats(); + optimizeShapeRegions(fn, mod, stats); + assert(stats.shape_guards_folded === 0, "stale compare must not fold"); + + // the same CFG with the compare minted fresh in t1 DOES fold + const fb2 = new FunctionBuilder("fresh", ["%env", "%this", "p"]); + const q = fb2.fn.entry!.params[2]!; + const t1b = fb2.newBlock("t1"); + const outb = fb2.newBlock("out"); + const ab = fb2.newBlock("a"); + const bbb = fb2.newBlock("b"); + const g = fb2.emit("has_shape", [q], { shape: shapeKey }); + fb2.condBr(g, t1b, [], outb, []); + fb2.sealBlock(t1b); + fb2.setInsertPoint(t1b); + const fresh = fb2.emit("has_shape", [q], { shape: shapeKey }); + fb2.condBr(fresh, ab, [], bbb, []); + fb2.sealBlock(ab); + fb2.sealBlock(bbb); + fb2.setInsertPoint(ab); + fb2.br(outb, []); + fb2.setInsertPoint(bbb); + fb2.br(outb, []); + fb2.sealBlock(outb); + fb2.setInsertPoint(outb); + fb2.ret(fb2.constUndefined()); + const fn2 = fb2.finish(); + const mod2 = new Module("fresh_mod"); + mod2.addFunction(fn2); + mod2.internShape([ + { name: "x", repr: "f64" }, + { name: "y", repr: "f64" }, + ]); + const stats2 = shapeOptStats(); + optimizeShapeRegions(fn2, mod2, stats2); + assert(stats2.shape_guards_folded === 1, "fresh dominated compare must fold"); + verifyModule(mod2); +}); + +// --- typed slots + heterogeneous fusion ------------------------ + +test("shapes-typed: f64 loads are raw + boxed at the exit; stores unbox", () => { + const g = lowerWithOracle("function f(p) { return p.x; }", stubShapeOracle({ p: PXY })); + assertContains(g.printed, ": f64 = slot_load"); + assertContains(g.printed, "box_f64"); + const s = lowerWithOracle("function f(p, v) { p.x = v; }", stubShapeOracle({ p: PXY })); + assertContains(s.printed, "unbox_f64"); + // boxed fields keep boxed access — no raw traffic anywhere + const b = lowerWithOracle("function f(p) { return p.s; }", stubShapeOracle({ p: PXY })); + assertNotContains(b.printed, "box_f64"); + assertNotContains(b.printed, ": f64 = slot_load"); +}); + +test("shapes-typed: shape and numeric regions fuse unboxed end-to-end", () => { + // the real oracle types f64-field member reads as {number}, which is + // what makes lowering wrap the arithmetic in numeric diamonds — the + // stub must too, or there is no numeric region to fuse + const oracle: TypeOracle = { + ...stubShapeOracle({ p: PXY }), + typeOfNode: (n) => { + const t = (n as { type?: string }).type; + return t === "MemberExpression" ? { tags: new Set(["number"]) } : { tags: "top" }; + }, + }; + const r = lowerFunctionNode( + parseFn("function f(p) { return p.x * p.x + p.y * p.y; }"), + undefined, + oracle + ); + verifyModule(r.module); + const stats = optimizeFunction(r.fn, r.module); + verifyModule(r.module); + const printed = printFunction(r.fn); + assert(stats.shape_numeric_merged >= 2, `het merges=${stats.shape_numeric_merged}`); + assert(stats.shape_regions_merged >= 2, `shape merges=${stats.shape_regions_merged}`); + const guards = (printed.match(/has_shape/g) || []).length; + assert(guards === 1, `expected 1 surviving has_shape, got ${guards}`); + const tags = (printed.match(/has_tag/g) || []).length; + assert(tags === 0, `expected every has_tag folded, got ${tags}`); + const rawLoads = (printed.match(/: f64 = slot_load/g) || []).length; + assert(rawLoads === 4, `expected 4 raw slot_loads, got ${rawLoads}`); + assert(stats.raw_join_params >= 2, `raw join params=${stats.raw_join_params}`); +}); + +// re-execution attack: region1's slow chain holds a generic mul fed by a +// get of a BOXED-repr field — re-running it after the fast side is not +// provably pure, so any merge below must refuse. The identical CFG with +// the field repr'd f64 is the control: it must merge. +function buildReexecAttack(sRepr: "boxed" | "f64"): OptStats { + const shapeKey = `x:f64,s:${sRepr}`; + const fb = new FunctionBuilder("reexec", ["%env", "%this", "p"]); + const p = fb.fn.entry!.params[2]!; + + const fast1 = fb.newBlock("fast1"); + const slow1 = fb.newBlock("slow1"); + const j1 = fb.newBlock("j1"); + const v1 = j1.addParam("v1"); + const g1 = fb.emit("has_shape", [p], { shape: shapeKey }); + fb.condBr(g1, fast1, [], slow1, []); + fb.sealBlock(fast1); + fb.sealBlock(slow1); + fb.setInsertPoint(fast1); + const lx = fb.emit("slot_load", [p], { shape: shapeKey, slot: 0, repr: "f64" }); + lx.type = "f64"; + const ls = fb.emit("slot_load", [p], { shape: shapeKey, slot: 1, repr: sRepr }); + let sraw: Inst; + if (sRepr === "boxed") { + sraw = fb.emit("unbox_f64", [ls], {}); + } else { + ls.type = "f64"; + sraw = ls; + } + const m = fb.emit("f64_mul", [lx, sraw], {}); + fb.br(j1, [fb.emit("box_f64", [m], {})]); + fb.setInsertPoint(slow1); + const gx = fb.emit("get_prop_atom", [p], { atom: "x" }); + const gs = fb.emit("get_prop_atom", [p], { atom: "s" }); + const mslow = fb.emit("mul", [gx, gs], {}); + fb.br(j1, [mslow]); + fb.sealBlock(j1); + fb.setInsertPoint(j1); + + const fast2 = fb.newBlock("fast2"); + const slow2 = fb.newBlock("slow2"); + const j2 = fb.newBlock("j2"); + const v2 = j2.addParam("v2"); + const g2 = fb.emit("has_shape", [p], { shape: shapeKey }); + fb.condBr(g2, fast2, [], slow2, []); + fb.sealBlock(fast2); + fb.sealBlock(slow2); + fb.setInsertPoint(fast2); + const l2 = fb.emit("slot_load", [p], { shape: shapeKey, slot: 0, repr: "f64" }); + l2.type = "f64"; + fb.br(j2, [fb.emit("box_f64", [l2], {})]); + fb.setInsertPoint(slow2); + const g2x = fb.emit("get_prop_atom", [p], { atom: "x" }); + fb.br(j2, [g2x]); + fb.sealBlock(j2); + fb.setInsertPoint(j2); + fb.ret(fb.emit("add", [v1, v2], {})); + + const fn = fb.finish(); + const mod = new Module("reexec_mod"); + mod.addFunction(fn); + mod.internShape([ + { name: "x", repr: "f64" }, + { name: "s", repr: sRepr }, + ]); + verifyModule(mod); + const stats = shapeOptStats(); + optimizeShapeRegions(fn, mod, stats); + verifyModule(mod); + return stats; +} + +test("shapes-typed: a boxed-field get feeding slow arithmetic refuses re-execution", () => { + const refused = buildReexecAttack("boxed"); + assert( + refused.shape_regions_merged === 0 && refused.shape_numeric_merged === 0, + "boxed-fed slow arithmetic must refuse the merge" + ); + const control = buildReexecAttack("f64"); + assert(control.shape_regions_merged === 1, "the f64-repr control must merge"); +}); + +// --- born with their shape ----------------------------------- + +test("born-shaped: a static literal lowers to make_object_shaped under --types", () => { + const { printed } = lowerWithOracle( + "function f(a) { return { x: 1, y: a }; }", + stubOracle({ a: ["number"] }) + ); + assertContainsOp(printed, "make_object_shaped"); + assertContains(printed, 'shape="x:f64,y:f64"'); + assertNotContainsOp(printed, "make_object"); +}); + +test("born-shaped: flag-off (null oracle) mints all-boxed shapes", () => { + // keys are static truth, so a null oracle still lowers born-shaped + // (gc-P5 part 2) — the reprs just stay boxed without type evidence + const { printed } = lowerWithOracle("function f(a) { return { x: 1, y: a }; }", null); + assertContainsOp(printed, "make_object_shaped"); + assertContains(printed, 'shape="x:boxed,y:boxed"'); + assertNotContainsOp(printed, "make_object"); +}); + +test("born-shaped: -fno-born-shaped restores make_object", () => { + withPassConfig({ bornShaped: false }, () => { + const { printed } = lowerWithOracle( + "function f() { return { x: 1, y: 2 }; }", + stubOracle({}) + ); + assertNotContains(printed, "make_object_shaped"); + }); +}); + +test("born-shaped: index-looking and duplicate keys decline to make_object", () => { + const dup = lowerWithOracle('function f() { return { x: 1, x: 2 }; }', stubOracle({})); + assertNotContains(dup.printed, "make_object_shaped"); + const idx = lowerWithOracle('function f() { return { "0": 1, y: 2 }; }', stubOracle({})); + assertNotContains(idx.printed, "make_object_shaped"); +}); + +test("born-shaped: computed keys / accessors / __proto__ keep the store path", () => { + const comp = lowerWithOracle("function f(k) { return { [k]: 1, y: 2 }; }", stubOracle({})); + assertNotContains(comp.printed, "make_object_shaped"); + const acc = lowerWithOracle( + "function f() { return { get x() { return 1; } }; }", + stubOracle({}) + ); + assertNotContains(acc.printed, "make_object_shaped"); + const proto = lowerWithOracle( + "function f(p) { return { __proto__: p, y: 2 }; }", + stubOracle({}) + ); + assertNotContains(proto.printed, "make_object_shaped"); +}); + +test("ctor-fill: a straight-line this-store prefix lowers to the guarded fill", () => { + const { printed } = lowerWithOracle( + "function Pt(x, y) { this.x = x; this.y = y; }", + stubOracle({ x: ["number"], y: ["number"] }) + ); + assertContains(printed, 'has_shape'); + assertContains(printed, 'shape=""'); // the empty-shape guard + assertContains(printed, "fill_object_shaped"); + assertContains(printed, 'shape="x:f64,y:f64"'); + assertContains(printed, "ctor_fill_slow"); + assertContains(printed, "set_prop_atom"); // the sequential slow arm survives +}); + +test("ctor-fill: flag-off keeps the sequential stores exactly", () => { + const { printed } = lowerWithOracle("function Pt(x, y) { this.x = x; this.y = y; }", null); + assertNotContains(printed, "fill_object_shaped"); + assertNotContains(printed, "has_shape"); + assertContains(printed, "set_prop_atom"); +}); + +test("ctor-fill: a call-valued store cuts the prefix (fence, oracle-free)", () => { + // `this.y = g()` could observe the receiver via g — the prefix must + // stop before it even though a lying oracle calls everything a number + const { printed } = lowerWithOracle( + "function Pt(x, g) { this.x = x; this.y = g(); }", + stubOracle({ x: ["number"], y: ["number"], g: ["number"] }) + ); + assertNotContains(printed, "fill_object_shaped"); +}); + +test("ctor-fill: `in` mid-prefix cuts the batch (the mid-construction observable)", () => { + const { printed } = lowerWithOracle( + 'function Pt(x, y) { this.x = x; this.t = "y" in this; this.y = y; }', + stubOracle({ x: ["number"], y: ["number"] }) + ); + assertNotContains(printed, "fill_object_shaped"); +}); + +test("ctor-fill: an escaping receiver before the stores declines", () => { + const { printed } = lowerWithOracle( + "function Pt(x, g) { g(this); this.x = x; this.y = x; }", + stubOracle({ x: ["number"] }) + ); + assertNotContains(printed, "fill_object_shaped"); +}); + +test("ctor-fill: a single-store prefix stays sequential (threshold)", () => { + const { printed } = lowerWithOracle( + "function Pt(x) { this.x = x; }", + stubOracle({ x: ["number"] }) + ); + assertNotContains(printed, "fill_object_shaped"); +}); + +test("ctor-fill: -fno-born-shaped disables the fill diamond", () => { + withPassConfig({ bornShaped: false }, () => { + const { printed } = lowerWithOracle( + "function Pt(x, y) { this.x = x; this.y = y; }", + stubOracle({}) + ); + assertNotContains(printed, "fill_object_shaped"); + }); +}); + +// --- born-shaped verifier rules (hand-built attack IR) -------------------------- + +interface FillAttackOpts { + guarded?: boolean; // guard the fill with has_shape(recv, "") (default true) + killInFast?: boolean; // a call between the guard and the fill + wrongCount?: boolean; // operand count != shape field count + guardShape?: string; // guard against this shape instead of "" +} + +function buildFillAttack(o: FillAttackOpts): Module { + const guarded = o.guarded !== false; + const fb = new FunctionBuilder("fillattack", ["%env", "%this", "a", "b"]); + const recv = fb.fn.entry!.params[1]!; + const a = fb.fn.entry!.params[2]!; + const bV = fb.fn.entry!.params[3]!; + const shapeKey = "x:boxed,y:boxed"; + + const fast = fb.newBlock("fast"); + const slow = fb.newBlock("slow"); + const join = fb.newBlock("join"); + const cond = guarded + ? fb.emit("has_shape", [recv], { shape: o.guardShape ?? "" }) + : fb.emit("to_boolean", [recv], {}); + fb.condBr(cond, fast, [], slow, []); + fb.sealBlock(fast); + fb.sealBlock(slow); + + fb.setInsertPoint(fast); + if (o.killInFast) fb.emit("call_runtime", [], { name: "ToString" }); + const vals = o.wrongCount ? [a] : [a, bV]; + fb.emit("fill_object_shaped", [recv, ...vals], { shape: shapeKey }); + fb.br(join, []); + + fb.setInsertPoint(slow); + fb.emit("set_prop_atom", [recv, a], { atom: "x" }); + fb.emit("set_prop_atom", [recv, bV], { atom: "y" }); + fb.br(join, []); + + fb.sealBlock(join); + fb.setInsertPoint(join); + fb.ret(fb.constUndefined()); + + const mod = new Module("fillattack_mod"); + mod.addFunction(fb.finish()); + mod.internShape([]); + mod.internShape([ + { name: "x", repr: "boxed" }, + { name: "y", repr: "boxed" }, + ]); + return mod; +} + +test("born-verify: a guarded fill in the empty-guard's true arm verifies", () => { + verifyModule(buildFillAttack({})); +}); + +test("born-verify: a fill without the empty-shape fact is rejected", () => { + assertThrows(() => verifyModule(buildFillAttack({ guarded: false })), "empty shape"); +}); + +test("born-verify: a WRITE|CALL between guard and fill kills the fact", () => { + assertThrows(() => verifyModule(buildFillAttack({ killInFast: true })), "empty shape"); +}); + +test("born-verify: a non-empty guard shape does not license the fill", () => { + // guarding has_shape(recv, "x:boxed,y:boxed") proves the receiver is + // FULL, not empty — batching stores onto it would double-install + assertThrows( + () => verifyModule(buildFillAttack({ guardShape: "x:boxed,y:boxed" })), + "empty shape" + ); +}); + +test("born-verify: operand count must match the shape's field count", () => { + assertThrows(() => verifyModule(buildFillAttack({ wrongCount: true })), "values for shape"); +}); + +test("born-verify: make_object_shaped checks field count and known shape", () => { + const fb = new FunctionBuilder("mkattack", ["%env", "%this", "a"]); + const a = fb.fn.entry!.params[2]!; + fb.emit("make_object_shaped", [a], { shape: "x:boxed,y:boxed" }); + fb.ret(fb.constUndefined()); + const mod = new Module("mkattack_mod"); + mod.addFunction(fb.finish()); + mod.internShape([ + { name: "x", repr: "boxed" }, + { name: "y", repr: "boxed" }, + ]); + assertThrows(() => verifyModule(mod), "values for shape"); + + const fb2 = new FunctionBuilder("mkattack2", ["%env", "%this", "a"]); + const a2 = fb2.fn.entry!.params[2]!; + fb2.emit("make_object_shaped", [a2], { shape: "nope:boxed" }); + fb2.ret(fb2.constUndefined()); + const mod2 = new Module("mkattack2_mod"); + mod2.addFunction(fb2.finish()); + assertThrows(() => verifyModule(mod2), "unknown module shape"); +}); + +// the optimizer/verifier proof-strength hazard (found by +// types-bornshapewrong1): foldProvenGuards deletes a has_tag over a +// const-number join (`c ? 1 : 0`), uncovering the slot_store. The typed-store form's +// typed store dissolves the hazard class: the store takes a raw f64 +// (unbox under whatever proof lowering had), so no guard deletion can +// ever strip the proof — the TYPE is the proof. Pin both directions: +// the raw form verifies with no has_tag anywhere, the boxed form is +// rejected by type no matter what the join's edges carry. +function buildConstJoinStore(nonNumberEdge: boolean, raw = false): Module { + const fb = new FunctionBuilder("cjstore", ["%env", "%this", "p", "c"]); + const p = fb.fn.entry!.params[2]!; + const c = fb.fn.entry!.params[3]!; + const shapeKey = "x:f64,y:f64"; + const then_bb = fb.newBlock("then"); + const else_bb = fb.newBlock("else"); + const vjoin = fb.newBlock("vjoin"); + const v = vjoin.addParam("v"); + const fast = fb.newBlock("fast"); + const out = fb.newBlock("out"); + const cb = fb.emit("to_boolean", [c], {}); + fb.condBr(cb, then_bb, [], else_bb, []); + fb.sealBlock(then_bb); + fb.sealBlock(else_bb); + fb.setInsertPoint(then_bb); + fb.br(vjoin, [fb.constNumber(1)]); + fb.setInsertPoint(else_bb); + fb.br(vjoin, [nonNumberEdge ? fb.constUndefined() : fb.constNumber(0)]); + fb.sealBlock(vjoin); + fb.setInsertPoint(vjoin); + const g = fb.emit("has_shape", [p], { shape: shapeKey }); + fb.condBr(g, fast, [], out, []); + fb.sealBlock(fast); + fb.setInsertPoint(fast); + // no has_tag anywhere: the raw form's proof is the operand type + const stored = raw ? fb.emit("unbox_f64", [v], {}) : v; + fb.emit("slot_store", [p, stored], { shape: shapeKey, slot: 0, repr: "f64" }); + fb.br(out, []); + fb.sealBlock(out); + fb.setInsertPoint(out); + fb.ret(fb.constUndefined()); + const mod = new Module("cjstore_mod"); + mod.addFunction(fb.finish()); + mod.internShape([ + { name: "x", repr: "f64" }, + { name: "y", repr: "f64" }, + ]); + return mod; +} + +test("born-verify: a typed f64 store needs no has_tag, whatever the join", () => { + verifyModule(buildConstJoinStore(false, true)); + verifyModule(buildConstJoinStore(true, true)); +}); + +test("born-verify: a boxed value into an f64 slot rejects by type", () => { + assertThrows(() => verifyModule(buildConstJoinStore(false)), "raw f64"); + assertThrows(() => verifyModule(buildConstJoinStore(true)), "raw f64"); +}); + +// --- shaped-literal sinking ----------------------------------- + +function lowerShapedSink(src: string): { fn: Func; printed: string; stats: OptStats } { + const r = lowerFunctionNode( + parseFn(src), + undefined, + stubShapeOracle({ o: PXY }, { a: ["number"] }) + ); + verifyModule(r.module); + const stats = optimizeFunction(r.fn, r.module); + verifyModule(r.module); + return { fn: r.fn, printed: printFunction(r.fn), stats }; +} + +test("sink-shaped: a non-escaping guarded literal scalar-replaces completely", () => { + // o's literal is born with PXY's exact shape (a types as number, b is + // boxed); every read folds to an operand, every guard resolves, the + // allocation drains away + const { printed, stats } = lowerShapedSink( + "function f(a, b) { var o = { x: 1, y: a, s: b }; return o.x + o.y + o.s; }" + ); + assert(stats.shape_allocs_sunk === 1, `sunk=${stats.shape_allocs_sunk}`); + assert(stats.shape_guards_sunk >= 1, `guards=${stats.shape_guards_sunk}`); + assertNotContains(printed, "make_object_shaped"); + assertNotContains(printed, "has_shape"); + assertNotContains(printed, "slot_load"); + assertNotContains(printed, "get_prop_atom"); +}); + +test("sink-shaped: an escaping literal is untouched", () => { + const { printed, stats } = lowerShapedSink( + "function f(a, b) { var o = { x: 1, y: a, s: b }; return o; }" + ); + assert(stats.shape_allocs_sunk === 0, `sunk=${stats.shape_allocs_sunk}`); + assertContains(printed, "make_object_shaped"); +}); + +test("sink-shaped: a call-operand use escapes", () => { + const { printed, stats } = lowerShapedSink( + "function f(a, b, g) { var o = { x: 1, y: a, s: b }; g(o); return o.x; }" + ); + assert(stats.shape_allocs_sunk === 0, `sunk=${stats.shape_allocs_sunk}`); + assertContains(printed, "make_object_shaped"); +}); + +test("sink-shaped: a written literal flow-sinks through the generic arms (sinking-P3)", () => { + // the store's diamond guards fold FALSE (twin arms; sound under + // writes), the generic read folds to the written const, and the + // allocation drains + const { fn, printed, stats } = lowerShapedSink( + "function f(a, b) { var o = { x: 1, y: a, s: b }; o.x = 2; return o.x; }" + ); + assert(stats.flow_allocs_sunk === 1, `flow_sunk=${stats.flow_allocs_sunk}`); + assertNotContains(printed, "make_object_shaped"); + assertNotContains(printed, "slot_store"); + assertNotContains(printed, "set_prop_atom"); + assertNotContains(printed, "get_prop_atom"); + // the written const reaches the return (possibly through the read + // diamond's now-single-pred join param — LLVM collapses those) + assertContains(printed, 'value=2'); +}); + +test("sink-shaped: -fno-flow-sink restores the written-literal decline", () => { + withPassConfig({ flowSink: false }, () => { + const { printed, stats } = lowerShapedSink( + "function f(a, b) { var o = { x: 1, y: a, s: b }; o.x = 2; return o.x; }" + ); + assert(stats.shape_allocs_sunk === 0, `sunk=${stats.shape_allocs_sunk}`); + assertContains(printed, "make_object_shaped"); + }); +}); + +test("sink-shaped: a non-own read blocks removal but own reads still fold", () => { + const { printed, stats } = lowerShapedSink( + "function f(a, b) { var o = { x: 1, y: a, s: b }; return o.x + o.zzz; }" + ); + assert(stats.shape_allocs_sunk === 0, `sunk=${stats.shape_allocs_sunk}`); + assert(stats.reads_folded >= 1, `folded=${stats.reads_folded}`); + assertContains(printed, "make_object_shaped"); + assertContains(printed, 'atom="zzz"'); // the prototype read survives +}); + +test("sink-shaped: shape mismatch resolves guards to the generic arm and still sinks", () => { + // b is untyped, so the literal's y field is born boxed — its interned + // shape differs from PXY, every has_shape(o, PXY) is statically false, + // and the reads fold through the generic arm + const { printed, stats } = lowerShapedSink( + "function f(b, c) { var o = { x: 1, y: b, s: c }; return o.x + o.y; }" + ); + assert(stats.shape_allocs_sunk === 1, `sunk=${stats.shape_allocs_sunk}`); + assertNotContains(printed, "make_object_shaped"); + assertNotContains(printed, "has_shape"); + assertNotContains(printed, "slot_load"); +}); + +// unprovable-repr attack: the shape KEY matches but an f64 field's operand +// is not provably a number (only buildable by hand — lowering derives repr +// and provability from the same predicate). the guard must fold FALSE: +// folding true would feed a raw slot_load from a possibly-non-number. +test("sink-shaped: an unprovable f64 operand folds the guard to the generic arm", () => { + const fb = new FunctionBuilder("unprovable", ["%env", "%this", "v"]); + const v = fb.fn.entry!.params[2]!; + const shapeKey = "x:f64,y:f64"; + const alloc = fb.emit("make_object_shaped", [v, v], { shape: shapeKey }); + const fast = fb.newBlock("fast"); + const slow = fb.newBlock("slow"); + const j = fb.newBlock("j"); + const jp = j.addParam("r"); + const g = fb.emit("has_shape", [alloc], { shape: shapeKey }); + fb.condBr(g, fast, [], slow, []); + fb.sealBlock(fast); + fb.sealBlock(slow); + fb.setInsertPoint(fast); + const l = fb.emit("slot_load", [alloc], { shape: shapeKey, slot: 0, repr: "f64" }); + l.type = "f64"; + fb.br(j, [fb.emit("box_f64", [l], {})]); + fb.setInsertPoint(slow); + fb.br(j, [fb.emit("get_prop_atom", [alloc], { atom: "x" })]); + fb.sealBlock(j); + fb.setInsertPoint(j); + fb.ret(jp); + const fn = fb.finish(); + const mod = new Module("unprovable_mod"); + mod.addFunction(fn); + mod.internShape([ + { name: "x", repr: "f64" }, + { name: "y", repr: "f64" }, + ]); + verifyModule(mod); + const stats = optimizeFunction(fn, mod); + verifyModule(mod); + const printed = printFunction(fn); + assert(stats.shape_guards_sunk === 1, `guards=${stats.shape_guards_sunk}`); + assert(stats.shape_allocs_sunk === 1, `sunk=${stats.shape_allocs_sunk}`); + // the raw fast arm must be gone (folding true would have kept it) + assertNotContains(printed, "slot_load"); + assertNotContains(printed, "make_object_shaped"); + assertNotContains(printed, "get_prop_atom"); // generic arm folded to v +}); + +test("sink-shaped: -fno-shaped-sink leaves the allocation alone", () => { + withPassConfig({ shapedSink: false }, () => { + const { printed, stats } = lowerShapedSink( + "function f(a, b) { var o = { x: 1, y: a, s: b }; return o.x + o.y; }" + ); + assert(stats.shape_allocs_sunk === 0, `sunk=${stats.shape_allocs_sunk}`); + assertContains(printed, "make_object_shaped"); + }); +}); + +// --- flow-sensitive sinking + partial escapes (sinking-P3) ------------------ + +test("sink-flow: writes across branches fold through a minted join param", () => { + let { printed } = lowerAndOptimize( + "function f(c, x, y) { let o = { a: 0 }; if (c) o.a = x; else o.a = y; return o.a; }" + ); + assertNotContains(printed, "make_object"); + assertNotContains(printed, "set_prop_atom"); + assertNotContains(printed, "get_prop_atom"); +}); + +test("sink-flow: a loop accumulator object drains (loop-carried param)", () => { + let { printed } = lowerAndOptimize( + "function f(n) { let o = { sum: 0 }; for (let i = 0; i < n; i = i + 1) o.sum = o.sum + i; return o.sum; }" + ); + assertNotContains(printed, "make_object"); + assertNotContains(printed, "set_prop_atom"); + assertNotContains(printed, "get_prop_atom"); +}); + +test("sink-flow: a read before the write sees the initial value", () => { + let { fn, printed } = lowerAndOptimize( + "function f(x) { let o = { a: 5 }; let r = o.a; o.a = x; return r; }" + ); + assertNotContains(printed, "make_object"); + let ret: Inst | null = null; + fn.forEachInst((i) => { if (i.op === "return") ret = i; }); + assert( + ret!.operands[0]!.op === "const" && ret!.operands[0]!.imms.value === 5, + `expected the initial 5, got ${ret!.operands[0]!.op}` + ); +}); + +test("sink-flow: single escape materializes at the escape site", () => { + // the write is baked into the materialized literal; the original + // allocation and store are gone but a make_object survives AT the + // call + let { fn, printed } = lowerAndOptimize( + "function f(g, x) { let o = { a: 1 }; o.a = x; g(o); return 0; }" + ); + assertContainsOp(printed, "make_object_shaped"); + assertNotContains(printed, "set_prop_atom"); + // the materialized literal's operand is the written value (param x) + let made: Inst | null = null; + fn.forEachInst((i) => { if (i.op === "make_object_shaped") made = i; }); + assert(made!.operands[0]!.op === "blockparam", "materialized field should be the written x"); +}); + +test("sink-flow: refusals leave the object alone", () => { + const cases: [string, string][] = [ + // a read reachable from the escape (the alias could mutate) + ["use after escape", "function f(g) { let o = { a: 1 }; o.a = 2; g(o); return o.a; }"], + // the escape can re-execute without re-executing the alloc + ["escape in loop", "function f(g, n) { let o = { a: 1 }; o.a = 2; for (let i = 0; i < n; i = i + 1) g(o); return 0; }"], + // two distinct escape instructions + ["two escapes", "function f(g, h, c) { let o = { a: 1 }; o.a = 2; if (c) g(o); else h(o); return 0; }"], + // key-adding write ([[Set]] walks the prototype chain) + ["key-adding write", "function f(x) { let o = { a: 1 }; o.b = x; return 0; }"], + // the escape instruction is itself a write (o.self = o) + ["self-write escape", "function f() { let o = { a: 1 }; o.a = o; return 0; }"], + ]; + for (const [name, src] of cases) { + let { printed } = lowerAndOptimize(src); + if (printed.indexOf("make_object") === -1) + throw new Error(`refusal '${name}' unexpectedly sank\n---\n${printed}\n---`); + } +}); + +test("sink-flow: a catch block in the rename region declines", () => { + let { printed } = lowerAndOptimize( + "function f(x) { let o = { a: 1 }; try { o.a = x; } catch (e) { } return o.a; }" + ); + assertContainsOp(printed, "make_object_shaped"); +}); + +test("sink-flow: shaped partial escape materializes a shaped literal", () => { + const { printed, stats } = lowerShapedSink( + "function f(a, b, g) { var o = { x: 1, y: a, s: b }; o.x = 2; g(o); return 0; }" + ); + assert(stats.flow_allocs_sunk === 1, `flow_sunk=${stats.flow_allocs_sunk}`); + assert(stats.allocs_materialized === 1, `materialized=${stats.allocs_materialized}`); + assertContains(printed, "make_object_shaped"); // the materialized one + assertNotContains(printed, "slot_store"); + assertNotContains(printed, "set_prop_atom"); +}); + +// --- rest_args / args_obj length sinking (sinking-P3) ----------------------- + +test("sink-args: length-only arguments folds to arg_len and drains", () => { + let { printed } = lowerAndOptimize("function f() { return arguments.length; }"); + assertContains(printed, "arg_len"); + assertNotContains(printed, "args_obj"); +}); + +test("sink-args: length-only rest folds with its start index", () => { + let { printed } = lowerAndOptimize("function f(a, b, ...rest) { return rest.length; }"); + assertContains(printed, "arg_len"); + assertContains(printed, "index=2"); + assertNotContains(printed, "rest_args"); +}); + +test("sink-args: refusals keep the allocation", () => { + const cases = [ + "function f() { return arguments[0]; }", // computed read + "function f() { return arguments; }", // escape + "function f(...r) { r.length = 0; return r.length; }", // length write + "function f(...r) { return r.length + r[0]; }", // partial fold is not enough + ]; + for (const src of cases) { + let { printed } = lowerAndOptimize(src); + assertNotContains(printed, "arg_len"); + } +}); + +test("sink-args: -fno-args-sink leaves the allocation alone", () => { + withPassConfig({ argsSink: false }, () => { + let { printed } = lowerAndOptimize("function f() { return arguments.length; }"); + assertContains(printed, "args_obj"); + assertNotContains(printed, "arg_len"); + }); +}); + +// --- constructor-result sinking --------------------------------- + +// a hand-built module in the shape the sink requires: a fence-passing +// ctor, its closure stored once into promoted %self slot 0 by the +// toplevel, and a consumer constructing through the slot with guarded +// reads. the knobs each break exactly one screen. +interface CtorSinkOpts { + secondStore?: boolean; // a second store to the slot + protoWrite?: boolean; // a load used as a set_prop_atom base + trailingCtorCode?: boolean; // extra work after the ctor's fill join + swappedFill?: boolean; // fill operands not the formals in order + argcMismatch?: boolean; // construct passes fewer args than formals + escape?: boolean; // result also flows into a call + twoDiamonds?: boolean; // interleaved add whose value crosses the exit +} + +function buildCtorSinkModule(opts: CtorSinkOpts): { mod: Module; user: Func } { + const mod = new Module("ctor_sink_mod"); + const PXYKey = mod.internShape([ + { name: "x", repr: "f64" }, + { name: "y", repr: "f64" }, + ]); + mod.internShape([]); + + const cb = new FunctionBuilder("Point", ["%env", "%this", "x", "y"]); + const cthis = cb.fn.entry!.params[1]!; + const cx = cb.fn.entry!.params[2]!; + const cy = cb.fn.entry!.params[3]!; + const cfast = cb.newBlock("ctor_fill_fast"); + const cslow = cb.newBlock("ctor_fill_slow"); + const cjoin = cb.newBlock("ctor_fill_join"); + const cg = cb.emit("has_shape", [cthis], { shape: "" }); + cb.condBr(cg, cfast, [], cslow, []); + cb.sealBlock(cfast); + cb.sealBlock(cslow); + cb.setInsertPoint(cfast); + cb.emit("fill_object_shaped", opts.swappedFill ? [cthis, cy, cx] : [cthis, cx, cy], { + shape: PXYKey, + }); + cb.br(cjoin, []); + cb.setInsertPoint(cslow); + cb.emit("set_prop_atom", [cthis, cx], { atom: "x" }); + cb.emit("set_prop_atom", [cthis, cy], { atom: "y" }); + cb.br(cjoin, []); + cb.sealBlock(cjoin); + cb.setInsertPoint(cjoin); + if (opts.trailingCtorCode) cb.emit("get_prop_atom", [cthis], { atom: "x" }); + cb.ret(cb.constUndefined()); + mod.addFunction(cb.finish()); + + const tb = new FunctionBuilder("toplevel", ["%env", "%this"]); + const tenv = tb.fn.entry!.params[0]!; + const cl = tb.emit("make_closure", [tenv], { fn: "Point", name: "Point" }); + tb.emit("module_slot_store", [cl], { module: "%self", slot: 0 }); + if (opts.secondStore) tb.emit("module_slot_store", [cl], { module: "%self", slot: 0 }); + if (opts.protoWrite) { + const ld = tb.emit("module_slot_load", [], { module: "%self", slot: 0 }); + tb.emit("set_prop_atom", [ld, tb.constNumber(1)], { atom: "prototype" }); + } + tb.ret(tb.constUndefined()); + mod.addFunction(tb.finish()); + + const ub = new FunctionBuilder("user", ["%env", "%this", "g"]); + const gparam = ub.fn.entry!.params[2]!; + const ld = ub.emit("module_slot_load", [], { module: "%self", slot: 0 }); + const bx = ub.emit("box_f64", [ub.emit("f64_const", [], { value: 1 })], {}); + const by = ub.emit("box_f64", [ub.emit("f64_const", [], { value: 2 })], {}); + const p = ub.emit("construct", opts.argcMismatch ? [ld, bx] : [ld, bx, by], {}); + + const fast = ub.newBlock("shape_fast"); + const slow = ub.newBlock("shape_slow"); + const join = ub.newBlock("shape_join"); + const r = join.addParam("r"); + const pg = ub.emit("has_shape", [p], { shape: PXYKey }); + ub.condBr(pg, fast, [], slow, []); + ub.sealBlock(fast); + ub.sealBlock(slow); + ub.setInsertPoint(fast); + const sl = ub.emit("slot_load", [p], { shape: PXYKey, slot: 0, repr: "f64" }); + sl.type = "f64"; + ub.br(join, [ub.emit("box_f64", [sl], {})]); + ub.setInsertPoint(slow); + ub.br(join, [ub.emit("get_prop_atom", [p], { atom: "x" })]); + ub.sealBlock(join); + ub.setInsertPoint(join); + + if (opts.twoDiamonds) { + // a value defined between the diamonds and used past the exit — + // it must cross the epoch join through a minted param + const s = ub.emit("add", [r, bx], {}); + const fast2 = ub.newBlock("shape_fast2"); + const slow2 = ub.newBlock("shape_slow2"); + const join2 = ub.newBlock("shape_join2"); + const r2 = join2.addParam("r2"); + const pg2 = ub.emit("has_shape", [p], { shape: PXYKey }); + ub.condBr(pg2, fast2, [], slow2, []); + ub.sealBlock(fast2); + ub.sealBlock(slow2); + ub.setInsertPoint(fast2); + const sl2 = ub.emit("slot_load", [p], { shape: PXYKey, slot: 1, repr: "f64" }); + sl2.type = "f64"; + ub.br(join2, [ub.emit("box_f64", [sl2], {})]); + ub.setInsertPoint(slow2); + ub.br(join2, [ub.emit("get_prop_atom", [p], { atom: "y" })]); + ub.sealBlock(join2); + ub.setInsertPoint(join2); + ub.ret(ub.emit("add", [s, r2], {})); + } else { + if (opts.escape) ub.emit("call", [gparam, ub.constUndefined(), p], {}); + ub.ret(r); + } + const user = ub.finish(); + mod.addFunction(user); + return { mod, user }; +} + +function runCtorSink(opts: CtorSinkOpts = {}): { n: number; printed: string; stats: OptStats } { + const { mod, user } = buildCtorSinkModule(opts); + verifyModule(mod); + const n = sinkConstructResults(mod, new Set([0]), "toplevel"); + verifyModule(mod); + const stats = optimizeFunction(user, mod); + verifyModule(mod); + return { n, printed: printFunction(user), stats }; +} + +test("sink-ctor: a qualifying construct virtualizes behind the epoch check", () => { + const { n, printed, stats } = runCtorSink({}); + assert(n === 1, `sunk=${n}`); + assertContains(printed, "epoch_check"); + assertContains(printed, "construct"); // the slow arm keeps the real one + assertNotContains(printed, "make_object_shaped"); // the virtual arm drained + assert(stats.shape_allocs_sunk === 1, `allocs=${stats.shape_allocs_sunk}`); +}); + +test("sink-ctor: live-outs cross the epoch join through minted params", () => { + const { n, printed, stats } = runCtorSink({ twoDiamonds: true }); + assert(n === 1, `sunk=${n}`); + assertContains(printed, "epoch_check"); + assertNotContains(printed, "make_object_shaped"); + assert(stats.shape_allocs_sunk === 1, `allocs=${stats.shape_allocs_sunk}`); +}); + +test("sink-ctor: refusals leave the construct alone", () => { + const attacks: CtorSinkOpts[] = [ + { secondStore: true }, + { protoWrite: true }, + { trailingCtorCode: true }, + { swappedFill: true }, + { argcMismatch: true }, + { escape: true }, + ]; + for (const a of attacks) { + const { n, printed } = runCtorSink(a); + assert(n === 0, `${JSON.stringify(a)}: sunk=${n}`); + assertNotContains(printed, "epoch_check"); + } +}); + +test("sink-ctor: a non-promoted slot declines", () => { + const { mod, user } = buildCtorSinkModule({}); + verifyModule(mod); + const n = sinkConstructResults(mod, new Set(), "toplevel"); + assert(n === 0, `sunk=${n}`); + verifyModule(mod); + assertNotContains(printFunction(user), "epoch_check"); +}); + +test("sink-ctor: -fno-ctor-sink leaves the construct alone", () => { + withPassConfig({ ctorSink: false }, () => { + const { n, printed } = runCtorSink({}); + assert(n === 0, `sunk=${n}`); + assertNotContains(printed, "epoch_check"); + }); +}); + +// --- cleanup (compiler-P1): const folding, lattice, CSE, devirt ----------------- + +function optStatsOf(src: string): { fn: Func; printed: string; stats: OptStats } { + let { fn } = lowerOne(src); + const stats = optimizeFunction(fn); + verifyFunction(fn); + return { fn, printed: printFunction(fn), stats }; +} + +test("cleanup: numeric constant arithmetic folds", () => { + const { fn, stats } = optStatsOf("function f() { return 2 * 3 + 4; }"); + let ret: Inst | null = null; + fn.forEachInst((i) => { + if (i.op === "return") ret = i; + }); + assert(ret!.operands[0]!.op === "const", "return operand is a const"); + assert(ret!.operands[0]!.imms.value === 10, "2*3+4 folds to 10"); + assert(stats.consts_folded >= 2, `consts_folded=${stats.consts_folded}`); +}); + +test("cleanup: string concat folds only for string operands", () => { + const { fn } = optStatsOf('function f() { return "a" + "b"; }'); + let ret: Inst | null = null; + fn.forEachInst((i) => { + if (i.op === "return") ret = i; + }); + assert(ret!.operands[0]!.op === "const", "atom+atom folds"); + assert(ret!.operands[0]!.imms.value === "ab", "concat result"); + // number-to-string formatting is the runtime's business: no fold + const { printed } = optStatsOf('function f() { return "a" + 1; }'); + assertContains(printed, " = add "); +}); + +test("cleanup: typeof x === 'T' becomes typeof_is", () => { + const { printed, stats } = optStatsOf('function f(x) { return typeof x === "number"; }'); + assertContains(printed, "typeof_is"); + assertNotContains(printed, "strict_eq"); + assertNotContains(printed, " = typeof "); // dead typeof swept + assert(stats.typeof_rewrites === 1, `typeof_rewrites=${stats.typeof_rewrites}`); +}); + +test("cleanup: typeof of a lattice-known value folds to its tag", () => { + const { fn } = optStatsOf("function f() { return typeof 1; }"); + let ret: Inst | null = null; + fn.forEachInst((i) => { + if (i.op === "return") ret = i; + }); + assert(ret!.operands[0]!.imms.value === "number", "typeof 1 is 'number'"); + const o = optStatsOf("function f() { return typeof {}; }"); + let ret2: Inst | null = null; + o.fn.forEachInst((i) => { + if (i.op === "return") ret2 = i; + }); + assert(ret2!.operands[0]!.imms.value === "object", "typeof {} is 'object'"); +}); + +test("cleanup: if (!x) inverts the branch instead of calling not", () => { + const { printed, stats } = optStatsOf("function f(x) { if (!x) return 1; return 2; }"); + assertNotContains(printed, "logical_not"); + assert(stats.branches_folded >= 1, `branches_folded=${stats.branches_folded}`); +}); + +test("cleanup: known-truthiness conditions fold the branch", () => { + const { printed } = optStatsOf("function f() { if (null) return 1; return 2; }"); + assertNotContains(printed, "cond_br"); + const o = optStatsOf("function f() { if ({}) return 1; return 2; }"); + assertNotContains(o.printed, "cond_br"); + let ret: Inst | null = null; + o.fn.forEachInst((i) => { + if (i.op === "return" && ret === null) ret = i; + }); + assert(ret!.operands[0]!.imms.value === 1, "object condition is truthy"); +}); + +test("cleanup: lattice-proven number arithmetic lowers to f64 with no guard", () => { + // a*1 and b*1 are numbers by the mul result rule; the outer add + // then computes unboxed — with no oracle and no has_tag anywhere + const { printed, stats } = optStatsOf("function f(a, b) { return (a * 1) + (b * 1); }"); + assertContains(printed, "f64_add"); + assertNotContains(printed, "has_tag"); + assert(stats.lattice_arith >= 1, `lattice_arith=${stats.lattice_arith}`); +}); + +test("cleanup: unary_plus on a proven number is the identity", () => { + const { printed } = optStatsOf("function f(a) { return +(a * 1); }"); + assertNotContains(printed, "unary_plus"); +}); + +test("cleanup: proven-number compare feeds cond_br through f64_lt", () => { + const { printed } = optStatsOf( + "function f(a, b) { if (a * 1 < b * 1) return 1; return 2; }" + ); + assertContains(printed, "f64_lt"); + assertNotContains(printed, " = lt "); + assertNotContains(printed, "to_boolean"); +}); + +test("cleanup: trivial block params prune to their single value", () => { + const fb = new FunctionBuilder("f", ["%env", "%this"]); + const env = fb.fn.entry!.params[0]!; + const v = fb.constNumber(7); + const cond = fb.emit("to_boolean", [env], {}); + const a = fb.newBlock("a"); + const b = fb.newBlock("b"); + const j = fb.newBlock("join"); + const p = j.addParam("t"); + fb.condBr(cond, a, [], b, []); + fb.sealBlock(a); + fb.sealBlock(b); + fb.setInsertPoint(a); + fb.br(j, [v]); + fb.setInsertPoint(b); + fb.br(j, [v]); + fb.sealBlock(j); + fb.setInsertPoint(j); + fb.ret(p); + const fn = fb.finish(); + verifyFunction(fn); + const stats = optimizeFunction(fn); + verifyFunction(fn); + assert(stats.params_pruned === 1, `params_pruned=${stats.params_pruned}`); + let ret: Inst | null = null; + fn.forEachInst((i) => { + if (i.op === "return") ret = i; + }); + assert(ret!.operands[0] === v, "return sees the value directly"); +}); + +test("cleanup: -fno-eir-cleanup leaves the residue alone", () => { + withPassConfig({ eirCleanup: false }, () => { + const { printed, stats } = optStatsOf("function f() { return 2 * 3 + 4; }"); + assertContains(printed, " = mul "); + assert(stats.consts_folded === 0, `consts_folded=${stats.consts_folded}`); + }); +}); + +// --- module-slot load CSE ------------------------------------------------------- + +test("slot-cse: same-block reloads fold; a call kills availability", () => { + const fb = new FunctionBuilder("g", ["%env", "%this"]); + const l1 = fb.emit("module_slot_load", [], { module: "%self", slot: 0 }); + const l2 = fb.emit("module_slot_load", [], { module: "%self", slot: 0 }); + const sum1 = fb.emit("add", [l1, l2], {}); + fb.emit("call", [sum1, fb.constUndefined()], {}); + const l3 = fb.emit("module_slot_load", [], { module: "%self", slot: 0 }); + fb.ret(fb.emit("add", [sum1, l3], {})); + const fn = fb.finish(); + verifyFunction(fn); + const stats = optimizeFunction(fn); + verifyFunction(fn); + // l2 folds to l1; l3 survives the CALL kill + assert(stats.slot_loads_cse === 1, `slot_loads_cse=${stats.slot_loads_cse}`); + let loads = 0; + fn.forEachInst((i) => { + if (i.op === "module_slot_load") loads++; + }); + assert(loads === 2, `loads=${loads}`); +}); + +test("slot-cse: a stable slot's loads fold across calls and blocks", () => { + // toplevel: store the slot once in entry, read it on both sides of + // a call and across a diamond — every post-store load folds to the + // stored value + const mod = new Module("cse_mod"); + const fb = new FunctionBuilder("toplevel", ["%env", "%this"]); + const obj = fb.emit("make_object", [], { keys: [] }); + fb.emit("module_slot_store", [obj], { module: "%self", slot: 0 }); + const l1 = fb.emit("module_slot_load", [], { module: "%self", slot: 0 }); + fb.emit("call", [l1, fb.constUndefined()], {}); + const t = fb.newBlock("t"); + const f = fb.newBlock("f"); + const j = fb.newBlock("j"); + const cond = fb.emit("to_boolean", [fb.fn.entry!.params[0]!], {}); + fb.condBr(cond, t, [], f, []); + fb.sealBlock(t); + fb.sealBlock(f); + fb.setInsertPoint(t); + fb.br(j, []); + fb.setInsertPoint(f); + fb.br(j, []); + fb.sealBlock(j); + fb.setInsertPoint(j); + const l2 = fb.emit("module_slot_load", [], { module: "%self", slot: 0 }); + fb.ret(l2); + mod.addFunction(fb.finish()); + verifyModule(mod); + const stats = optimizeModule(mod, "toplevel"); + verifyModule(mod); + assert(stats.slot_loads_cse >= 2, `slot_loads_cse=${stats.slot_loads_cse}`); + let loads = 0; + mod.functions[0]!.forEachInst((i) => { + if (i.op === "module_slot_load") loads++; + }); + assert(loads === 0, `loads=${loads}`); +}); + +test("slot-cse: a suspendable function declines the stable exemptions", () => { + // a generator body (post-desugar: generator_yield runtime calls) + // can see the toplevel's remaining stores run mid-suspension — its + // loads must reload even for stable slots + const mod = new Module("cse_gen_mod"); + const fb = new FunctionBuilder("toplevel", ["%env", "%this"]); + const obj = fb.emit("make_object", [], { keys: [] }); + fb.emit("module_slot_store", [obj], { module: "%self", slot: 0 }); + fb.ret(fb.constUndefined()); + mod.addFunction(fb.finish()); + const gb = new FunctionBuilder("gen_body", ["%env", "%this"]); + const l1 = gb.emit("module_slot_load", [], { module: "%self", slot: 0 }); + gb.emit("call_runtime", [l1], { name: "generator_yield" }); + const l2 = gb.emit("module_slot_load", [], { module: "%self", slot: 0 }); + gb.ret(l2); + mod.addFunction(gb.finish()); + verifyModule(mod); + const stats = optimizeModule(mod, "toplevel"); + verifyModule(mod); + assert(stats.slot_loads_cse === 0, `slot_loads_cse=${stats.slot_loads_cse}`); + let loads = 0; + mod.functions[1]!.forEachInst((i) => { + if (i.op === "module_slot_load") loads++; + }); + assert(loads === 2, `loads=${loads}`); +}); + +test("slot-cse: a second store (an accessor setter) breaks stability", () => { + const mod = new Module("cse_mod2"); + const fb = new FunctionBuilder("toplevel", ["%env", "%this"]); + const obj = fb.emit("make_object", [], { keys: [] }); + fb.emit("module_slot_store", [obj], { module: "%self", slot: 0 }); + const l1 = fb.emit("module_slot_load", [], { module: "%self", slot: 0 }); + fb.emit("call", [l1, fb.constUndefined()], {}); + const l2 = fb.emit("module_slot_load", [], { module: "%self", slot: 0 }); + fb.ret(l2); + mod.addFunction(fb.finish()); + const sb = new FunctionBuilder("set_export_x", ["%env", "%this", "value"]); + sb.emit("module_slot_store", [sb.readVariable("value", sb.cur)], { + module: "%self", + slot: 0, + }); + sb.ret(sb.constUndefined()); + mod.addFunction(sb.finish()); + verifyModule(mod); + const stats = optimizeModule(mod, "toplevel"); + verifyModule(mod); + // the load after the CALL must reload (the setter may have run) + assert(stats.slot_loads_cse === 1, `slot_loads_cse=${stats.slot_loads_cse}`); +}); + +// --- devirtualization ----------------------------------------------------------- + +function buildDevirtModule(opts: { ctorMark?: boolean; envUse?: boolean }): { + mod: Module; + ssaCall: Inst; + slotCall: Inst; +} { + const mod = new Module("devirt_mod"); + + // the callee: returns 1; optionally touches its env + const hb = new FunctionBuilder("helper", ["%env", "%this"]); + if (opts.envUse) hb.emit("env_load", [hb.fn.entry!.params[0]!], { slot: 0 }); + hb.ret(hb.constNumber(1)); + mod.addFunction(hb.finish()); + + // toplevel: closure minted, stored to %self slot 0, called via SSA + const fb = new FunctionBuilder("toplevel", ["%env", "%this"]); + const env = fb.constUndefined(); + const clo = fb.emit("make_closure", [env], { fn: "helper", name: "helper" }); + fb.emit("module_slot_store", [clo], { module: "%self", slot: 0 }); + if (opts.ctorMark) fb.emit("call_runtime", [clo], { name: "set_constructor_kind_base" }); + const ssaCall = fb.emit("call", [clo, fb.constUndefined()], {}); + fb.ret(ssaCall); + mod.addFunction(fb.finish()); + + // another function calls through the slot + const gb = new FunctionBuilder("user", ["%env", "%this"]); + const load = gb.emit("module_slot_load", [], { module: "%self", slot: 0 }); + const slotCall = gb.emit("call", [load, gb.constUndefined()], {}); + gb.ret(slotCall); + mod.addFunction(gb.finish()); + + verifyModule(mod); + return { mod, ssaCall, slotCall }; +} + +test("devirt: SSA-visible and stable-slot call sites go direct", () => { + const { mod, ssaCall, slotCall } = buildDevirtModule({}); + const stats = devirtualizeModule(mod, "toplevel"); + verifyModule(mod); + assert(stats.ssa_sites === 1, `ssa_sites=${stats.ssa_sites}`); + assert(stats.slot_sites === 1, `slot_sites=${stats.slot_sites}`); + assert(ssaCall.imms.direct === "helper", "ssa site direct"); + assert(slotCall.imms.direct === "helper", "slot site direct"); + assert(slotCall.operands[0]!.op === "const", "slot site env is undefined const"); +}); + +test("devirt: a constructor-kind-marked closure declines", () => { + const { mod, ssaCall, slotCall } = buildDevirtModule({ ctorMark: true }); + const stats = devirtualizeModule(mod, "toplevel"); + verifyModule(mod); + assert(stats.ssa_sites === 0 && stats.slot_sites === 0, "no sites rewritten"); + assert(!ssaCall.imms.direct && !slotCall.imms.direct, "calls stay generic"); +}); + +test("devirt: an env-using callee declines the cross-function slot site", () => { + const { mod, ssaCall, slotCall } = buildDevirtModule({ envUse: true }); + const stats = devirtualizeModule(mod, "toplevel"); + verifyModule(mod); + // SSA site still fine (the env value is right there); slot site + // can't supply the env cross-function + assert(stats.ssa_sites === 1, `ssa_sites=${stats.ssa_sites}`); + assert(stats.slot_sites === 0, `slot_sites=${stats.slot_sites}`); + assert(ssaCall.imms.direct === "helper" && !slotCall.imms.direct, "only the ssa site"); +}); + +test("devirt: -fno-devirt leaves every site generic", () => { + withPassConfig({ devirt: false }, () => { + const { mod, ssaCall } = buildDevirtModule({}); + const stats = devirtualizeModule(mod, "toplevel"); + assert(stats.ssa_sites === 0 && stats.slot_sites === 0, "disabled"); + assert(!ssaCall.imms.direct, "call stays generic"); + }); +}); + +// -------------------------------------------------------------------------------- + +if (failures > 0) { + console.log(`${failures} test(s) FAILED`); + process.exit(1); +} else { + console.log("all EIR tests passed"); +} diff --git a/lib/eir/verifier.ts b/lib/eir/verifier.ts new file mode 100644 index 00000000..7c55f06b --- /dev/null +++ b/lib/eir/verifier.ts @@ -0,0 +1,641 @@ +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: + */ + +// EIR structural verifier. checks: +// - every opcode exists and respects its arity +// - every block is sealed and ends in exactly one terminator +// - branch edge argument counts match the target's parameter counts +// - every operand's definition dominates its use (standard iterative +// dominance computation over the CFG) +// +// verify() throws on the first violation; the error message names the +// function, block, and instruction involved. + +import { opInfo, isTerminator, Effect } from "./ops"; +import { printInst } from "./printer"; +import type { Func, Block, Inst, Module } from "./ir"; + +// --- shape guard facts ----------------------------------- +// +// The effect-kill soundness inventory for shape facts, in one place (this +// is THE hazard class shape facts add): +// +// - A fact "(value v, shape S)" means: on every path to here, a +// has_shape(v, S) compare executed, answered true, and NO instruction +// that can change any object's shape has run since. Under that fact a +// slot_load/slot_store on v at S-derived indices is safe: the guard +// proved v is an ordinary shaped object whose storage word is a slot +// array with at least S.fieldcount slots (a stale fact could leave the +// storage word a dictionary-mode MAP pointer — the addressing itself +// would be wrong, not just the value). +// - Facts are born on the TRUE edge of a cond_br whose condition is a +// has_shape defined in the SAME block with no kill between its +// definition and the branch ("fresh" — a compare separated from its +// branch by a call would prove the shape held BEFORE the call, not +// after). +// - Facts die at every instruction whose effects include WRITE or CALL: +// stores can transition/migrate the receiver, calls can run arbitrary +// JS. slot_store itself is a WRITE and kills — the "same-region store +// provably doesn't transition" refinement is deliberately NOT modeled +// (fail-closed; revisit with measurements). +// - Facts never cross unwind edges (the throwing instruction may have +// been mid-block, after arbitrary kills), so catch blocks start empty. +// - Join = set intersection over incoming edges (a must-analysis). +// - SSA immutability makes the VALUE part of a fact stable; only the +// heap side (the object's header) can move, which is exactly what the +// kill rule tracks. +// +// Number-tag facts (the BOXED slot_store's repr proof) need no kill rule: +// has_tag tests the VALUE's own tag, and SSA values are immutable — +// dominance alone suffices (tagFactDominates below, the guardFactAt shape +// from optimize-guards generalized to either edge). Typed slots: an +// f64-repr store takes a raw f64 operand, so its repr proof is the type +// system itself (a raw f64 is a number by construction) — the has_tag +// dominance requirement, and the provenNumberIntrinsic escape hatch that +// mirrored optimizer folds over it, are gone with the boxed f64 store. +// +// The engine is shared with optimize-guards' shape-fact folding: the +// optimizer folds on the same facts the verifier re-derives, so a fold the +// optimizer gets wrong is a fold the verifier rejects (trust-free, the +// raw-join discipline). + +const SHAPE_KILL = Effect.WRITE | Effect.CALL; + +export function shapeFactKey(valueId: number, shape: string): string { + return `${valueId}|${shape}`; +} + +function isShapeGuard(inst: Inst): boolean { + return inst.op === "has_shape"; +} + +export interface ShapeFactAnalysis { + // facts holding at entry of each reachable block + blockIn: Map>; + // facts holding immediately before insts[uptoIndex] of `block` + factsAt(block: Block, uptoIndex: number): Set; +} + +// forward must-dataflow of shape facts over the CFG. Cheap bail: returns +// null when the function has no has_shape at all (every flag-off compile). +export function computeShapeFacts(fn: Func): ShapeFactAnalysis | null { + const universe = new Set(); + fn.forEachInst((inst) => { + if (isShapeGuard(inst)) + universe.add(shapeFactKey(inst.operands[0]!.id, String(inst.imms["shape"]))); + }); + if (universe.size === 0) return null; + + const { rpo, reachable } = computeRPO(fn); + const blockIn = new Map>(); + for (const b of rpo) blockIn.set(b, b === fn.entry ? new Set() : new Set(universe)); + + // transfer IN through the block's instructions (kills only; facts are + // born on edges, not mid-block) + const transfer = (b: Block, facts: Set, uptoIndex: number): Set => { + let out = facts; + const n = Math.min(uptoIndex, b.insts.length); + for (let i = 0; i < n; i++) { + const inst = b.insts[i]!; + if ((opInfo(inst.op).effects & SHAPE_KILL) !== 0) { + if (out.size > 0) out = new Set(); + } + } + return out; + }; + + // the fact a specific outgoing edge adds: the TRUE edge of a cond_br on + // a same-block, still-fresh has_shape + const edgeGen = (b: Block, targetIndex: number): string | null => { + const term = b.terminator; + if (!term || term.op !== "cond_br" || targetIndex !== 0) return null; + const cond = term.operands[0]!; + if (!isShapeGuard(cond) || cond.block !== b) return null; + const gi = b.insts.indexOf(cond); + if (gi < 0) return null; + for (let i = gi + 1; i < b.insts.length; i++) { + if ((opInfo(b.insts[i]!.op).effects & SHAPE_KILL) !== 0) return null; // stale + } + return shapeFactKey(cond.operands[0]!.id, String(cond.imms["shape"])); + }; + + let changed = true; + while (changed) { + changed = false; + for (const b of rpo) { + if (b === fn.entry) continue; + let acc: Set | null = null; + for (const e of b.predEdges) { + const p = e.inst.block!; + if (!reachable.has(p)) continue; + const t = e.inst.targets![e.targetIndex]!; + let out: Set; + if (t.kind === "unwind") { + out = new Set(); // mid-block unwind: no facts survive + } else { + out = new Set(transfer(p, blockIn.get(p) ?? new Set(), p.insts.length)); + const gen = edgeGen(p, e.targetIndex); + if (gen) out.add(gen); + } + if (acc === null) acc = out; + else for (const f of acc) if (!out.has(f)) acc.delete(f); + } + const next = acc ?? new Set(); + const cur = blockIn.get(b)!; + if (next.size !== cur.size || [...next].some((f) => !cur.has(f))) { + blockIn.set(b, next); + changed = true; + } + } + } + + return { + blockIn, + factsAt: (block, uptoIndex) => + transfer(block, blockIn.get(block) ?? new Set(), uptoIndex), + }; +} + +// is there a dominating (wantTrue ? true : false)-edge fact of +// `has_tag(v, "number")` at `block`? Dominance-only: number-ness of an +// immutable SSA value is position-independent (see the inventory above). +export function tagFactDominates( + v: Inst, + wantTrue: boolean, + block: Block, + idom: Map +): boolean { + let b: Block = block; + for (;;) { + if (b.predEdges.length === 1) { + const e = b.predEdges[0]!; + if ( + e.inst.op === "cond_br" && + e.targetIndex === (wantTrue ? 0 : 1) && + e.inst.operands[0]!.op === "has_tag" && + e.inst.operands[0]!.imms["tag"] === "number" && + e.inst.operands[0]!.operands[0] === v + ) + return true; + } + const n = idom.get(b); + if (!n || n === b) return false; + b = n; + } +} + +export function computeRPO(fn: Func): { rpo: Block[]; reachable: Set } { + const entry = fn.entry!; + const visited = new Set(); + const postorder: Block[] = []; + // iterative dfs to keep the verifier usable on deep CFGs + const stack = [{ block: entry, succIndex: 0 }]; + visited.add(entry); + while (stack.length > 0) { + const frame = stack[stack.length - 1]!; + const succs = frame.block.succs(); + if (frame.succIndex < succs.length) { + const s = succs[frame.succIndex++]!; + if (!visited.has(s)) { + visited.add(s); + stack.push({ block: s, succIndex: 0 }); + } + } else { + postorder.push(frame.block); + stack.pop(); + } + } + return { rpo: postorder.slice().reverse(), reachable: visited }; +} + +// Cooper/Harvey/Kennedy "A Simple, Fast Dominance Algorithm" +export function computeDominators(fn: Func, rpo: Block[]): Map { + const entry = fn.entry!; + const index = new Map(); + rpo.forEach((b, i) => index.set(b, i)); + + const idom = new Map(); + idom.set(entry, entry); + + const intersect = (a: Block, b: Block): Block => { + while (a !== b) { + while (index.get(a)! > index.get(b)!) a = idom.get(a)!; + while (index.get(b)! > index.get(a)!) b = idom.get(b)!; + } + return a; + }; + + let changed = true; + while (changed) { + changed = false; + for (const b of rpo) { + if (b === entry) continue; + let newIdom: Block | null = null; + for (const p of b.preds()) { + if (!index.has(p)) continue; // unreachable pred + if (!idom.has(p)) continue; + newIdom = newIdom === null ? p : intersect(p, newIdom); + } + if (newIdom !== null && idom.get(b) !== newIdom) { + idom.set(b, newIdom); + changed = true; + } + } + } + return idom; +} + +export function dominates(idom: Map, a: Block, b: Block): boolean { + // does block a dominate block b? + let runner = b; + for (;;) { + if (runner === a) return true; + const next = idom.get(runner); + if (next === undefined || next === runner) return runner === a; + runner = next; + } +} + +export function verifyFunction(fn: Func, mod?: Module): boolean { + const vname = (v: Inst | null | undefined) => (v ? `%v${v.id}` : ""); + const fail = (msg: string, inst?: Inst): never => { + let where = ""; + if (inst) { + const inst_str = printInst(inst, vname); + where = ` at '${inst_str}'`; + } + throw new Error(`EIR verifier: fn @${fn.name}: ${msg}${where}`); + }; + + if (!fn.entry) fail("no entry block"); + + const { rpo, reachable } = computeRPO(fn); + const idom = computeDominators(fn, rpo); + + // per-block structural checks + for (const b of fn.blocks) { + if (!b.sealed) fail(`block ^${b.name} is not sealed`); + if (!reachable.has(b)) continue; // ignore unreachable blocks beyond seal check + + let term: Inst | null = null; + for (let i = 0; i < b.insts.length; i++) { + const inst = b.insts[i]!; + const info = opInfo(inst.op); // throws on unknown op + if (info.arity >= 0 && inst.operands.length !== info.arity) + fail(`'${inst.op}' has ${inst.operands.length} operands, wants ${info.arity}`, inst); + if (isTerminator(inst)) { + if (i !== b.insts.length - 1) fail(`terminator in the middle of ^${b.name}`, inst); + term = inst; + } + if (inst.op === "blockparam") fail("blockparam in instruction stream", inst); + } + if (!term) { + fail(`block ^${b.name} has no terminator`); + continue; + } + + // edge argument counts match target params (catch blocks' exception + // param is produced by unwinding, not passed on the edge) + if (term.targets) { + for (const t of term.targets) { + let expected = t.block.params.length; + if (t.block.isCatch) { + if (t.kind !== "unwind") + fail(`non-unwind edge into catch block ^${t.block.name}`, term); + if (t.block.params.length === 0 || !t.block.params[0]!.isException) + fail(`catch block ^${t.block.name} missing its exception param`, term); + expected -= 1; + } else if (t.kind === "unwind") { + fail(`unwind edge into non-catch block ^${t.block.name}`, term); + } + if (t.args.length !== expected) + fail( + `edge to ^${t.block.name} passes ${t.args.length} args, target wants ${expected}`, + term + ); + for (const a of t.args) + if (a === null || a === undefined) + fail(`edge to ^${t.block.name} has an unfilled argument`, term); + } + } + } + + // def-dominates-use. a value used as an operand must be defined in a + // block that dominates the use block (params count as defined at block + // entry; straight-line order enforced within a block). + const instIndex = new Map(); + for (const b of fn.blocks) { + b.insts.forEach((inst, i) => instIndex.set(inst, i)); + } + + const checkUse = (val: Inst | null, userBlock: Block, userIdx: number, inst: Inst): void => { + if (!val) { + fail("null operand", inst); + return; + } + if (val.removed) fail("use of removed block parameter", inst); + const defBlock = val.block!; + if (!reachable.has(defBlock)) fail("operand defined in unreachable block", inst); + if (defBlock === userBlock) { + if (val.op === "blockparam") return; // defined at entry of the block + const defIdx = instIndex.get(val); + if (defIdx === undefined || defIdx >= userIdx) + fail(`operand %v${val.id} used before definition`, inst); + } else { + if (!dominates(idom, defBlock, userBlock)) + fail( + `operand %v${val.id} (def in ^${defBlock.name}) does not dominate use in ^${userBlock.name}`, + inst + ); + } + }; + + for (const b of fn.blocks) { + if (!reachable.has(b)) continue; + b.insts.forEach((inst, i) => { + for (const o of inst.operands) checkUse(o, b, i, inst); + if (inst.targets) { + for (const t of inst.targets) for (const a of t.args) checkUse(a, b, i, inst); + } + }); + } + + // typed-flow rules (the low tier). f64/i1 values are raw machine values: + // - an op with a sig gets exactly what the sig says per slot ("f64" + // slots take only f64 values; "ejsval" slots take any boxed value, + // which excludes f64/i1); + // - an op without a sig takes only boxed values — with one exception: + // cond_br's condition may additionally be i1 (has_tag / f64_lt; the + // legacy "any"-typed condition sources to_boolean / prop_iter_next + // already emit their own machine i1); + // - branch-edge arguments must be boxed: block params are EjsValue + // phis in the emitter, so f64/i1 may NOT cross block boundaries. + // (guarded diamonds carry values across joins boxed.) + // The FIRST controlled exception: a param carrying the + // optimizer's rawJoin marker (Inst.rawJoin) is an f64-typed phi + // (double in the emitter) and takes exactly f64 arguments. The + // marker is provenance, not trust — the full safety conditions + // are re-checked here, so the strict rule stays in force for + // every lowering-created edge: lowering never sets the marker, + // and an f64 param WITHOUT it is rejected outright. i1 never + // crosses a block boundary under any rule. + // Exception-safety: a rawJoin param can never materialize an f64 + // in a handler entry — catch blocks and unwind edges are + // rejected below — and an f64 value can never be *treated as* an + // ejsval in a handler (or anywhere), because every ejsval-taking + // slot and every boxed param rejects f64-typed operands/args. + // The SECOND controlled exception: a specialized clone's + // ENTRY blockparam is f64 exactly when the function's sig types + // the matching formal f64 (env/this stay boxed); its `return` + // operand type must equal the sig's result; and every call_typed + // is re-checked against the callee Func's sig below (module-level, + // when the module is available). + const isRaw = (t: string) => t === "f64" || t === "i1"; + const sigParamType = (b: Block, p: Inst): "any" | "f64" | null => { + if (b !== fn.entry || !fn.sig) return null; + const formalIdx = p.paramIndex - 2; // entry params: [%env, %this, ...formals] + if (formalIdx < 0 || formalIdx >= fn.sig.formals.length) return null; + return fn.sig.formals[formalIdx]!; + }; + for (const b of fn.blocks) { + if (!reachable.has(b)) continue; + for (const p of b.params) { + if (p.type === "f64" && sigParamType(b, p) === "f64") continue; // typed formal + if (p.type === "f64") { + if (!p.rawJoin) + fail(`f64 block param without the optimizer's rawJoin marker`, p); + if (b.isCatch || p.isException) + fail(`rawJoin f64 param on a catch block / exception param`, p); + for (const e of b.predEdges) { + const t = e.inst.targets![e.targetIndex]!; + if (t.kind === "unwind") fail(`rawJoin f64 param fed by an unwind edge`, p); + const a = t.args[b.argIndexOfParam(p)]; + if (a && a.type !== "f64") + fail(`rawJoin f64 param receives a ${a.type} argument`, e.inst); + } + } else if (p.rawJoin) { + fail(`rawJoin marker on a non-f64 block param`, p); + } + } + for (const inst of b.insts) { + const info = opInfo(inst.op); + + // branch-edge arguments (checked FIRST: the op-specific cases + // below `continue` past the operand rules) + if (inst.targets) + for (const t of inst.targets) + t.args.forEach((a, i) => { + if (!a) return; + const param = t.block.params[i + (t.block.isCatch ? 1 : 0)]; + if (a.type === "f64") { + if (!param || !param.rawJoin || param.type !== "f64") + fail( + `edge to ^${t.block.name} passes a raw ${a.type} value; block arguments must be boxed`, + inst + ); + } else if (a.type === "i1") { + fail( + `edge to ^${t.block.name} passes a raw ${a.type} value; block arguments must be boxed`, + inst + ); + } else if (param && param.type === "f64") { + fail( + `edge to ^${t.block.name} passes a boxed value to an f64 param`, + inst + ); + } + }); + + // call_typed is typed by its CALLEE's sig, which a + // per-op table can't express. operand 0 (env) stays boxed; + // the argument slots must match the callee's formals exactly, + // and the instruction's stamped result type must equal the + // callee sig's result. Without a module (standalone + // verifyFunction) the callee can't be resolved; the boxed-env + // and no-i1 rules still hold. + if (inst.op === "call_typed") { + const calleeName = inst.imms["fn"] as string; + const callee = mod ? mod.functions.find((f) => f.name === calleeName) : undefined; + if (mod) { + if (!callee) fail(`call_typed to unknown function '${calleeName}'`, inst); + if (!callee!.sig) fail(`call_typed to un-sigged function '${calleeName}'`, inst); + const formals = callee!.sig!.formals; + if (inst.operands.length - 1 !== formals.length) + fail( + `call_typed passes ${inst.operands.length - 1} args, ` + + `callee sig wants ${formals.length}`, + inst + ); + const wantResult = callee!.sig!.result === "f64" ? "f64" : "any"; + if (inst.type !== wantResult) + fail(`call_typed result type ${inst.type} != callee sig ${wantResult}`, inst); + } + inst.operands.forEach((o, idx) => { + if (idx === 0) { + if (isRaw(o.type)) + fail(`call_typed env operand must be boxed, got ${o.type}`, inst); + return; + } + if (o.type === "i1") fail(`call_typed operand ${idx} may not be i1`, inst); + if (callee && callee.sig) { + const want = callee.sig.formals[idx - 1]!; + if (want === "f64" ? o.type !== "f64" : isRaw(o.type)) + fail( + `call_typed operand ${idx} wants ${want}, got ${o.type}`, + inst + ); + } + }); + continue; + } + // typed slots: slot ops are typed by their repr immediate, + // which a per-op table can't express (the call_typed precedent). + // The receiver is always boxed; an f64-repr store takes exactly + // a raw f64 (the type system IS the repr proof), a boxed-repr + // store takes a boxed value. slot_load's result stamp is + // checked against the repr in the shape section below. + if (inst.op === "slot_store") { + if (isRaw(inst.operands[0]!.type)) + fail(`slot_store receiver must be boxed, got ${inst.operands[0]!.type}`, inst); + const v = inst.operands[1]!; + if (inst.imms["repr"] === "f64") { + if (v.type !== "f64") + fail(`slot_store repr "f64" wants a raw f64 value, got ${v.type}`, inst); + } else if (isRaw(v.type)) { + fail(`slot_store repr "boxed" wants a boxed value, got ${v.type}`, inst); + } + continue; + } + // a sigged function's `return` must produce exactly + // the sig's result type (f64 result -> raw f64 operand) + if (inst.op === "return" && fn.sig && fn.sig.result === "f64") { + const o = inst.operands[0]!; + if (o.type !== "f64") + fail(`return in an f64-result function got ${o.type}`, inst); + continue; + } + + inst.operands.forEach((o, idx) => { + const want = info.sig ? info.sig.params[idx] : undefined; + if (want === "f64") { + if (o.type !== "f64") + fail(`'${inst.op}' operand ${idx} wants f64, got ${o.type}`, inst); + } else if (want === "ejsval") { + if (isRaw(o.type)) + fail(`'${inst.op}' operand ${idx} wants a boxed value, got ${o.type}`, inst); + } else if (inst.op === "cond_br" && idx === 0) { + if (o.type === "f64") fail("cond_br condition may not be f64", inst); + } else if (isRaw(o.type)) { + fail(`'${inst.op}' operand ${idx} may not be ${o.type}`, inst); + } + }); + } + } + + // --- shape-guarded slot access ----------------------- + // Every slot op must sit under an un-killed dominating has_shape fact on + // the same value for the same shape (see the effect-kill inventory at the + // top of this file); stores additionally prove the stored value's repr + // matches the field's — by TYPE for f64 (the typed-flow rule above), by + // a has_tag=false dominance fact for boxed — so compiled stores never + // owe a transition. slot_load's result stamp must agree with its + // repr (raw f64 loads are only meaningful under the guard's repr proof). + // With a module in hand, imms are checked against the module shape table + // (bounds, repr identity, known key). + let shapeFacts: ShapeFactAnalysis | null | undefined; + for (const b of fn.blocks) { + if (!reachable.has(b)) continue; + b.insts.forEach((inst, i) => { + const isSlotOp = inst.op === "slot_load" || inst.op === "slot_store"; + const isBornOp = inst.op === "make_object_shaped" || inst.op === "fill_object_shaped"; + if (!isSlotOp && !isBornOp && inst.op !== "has_shape") return; + const shapeImm = String(inst.imms["shape"]); + const fields = mod ? mod.shapes.get(shapeImm) : undefined; + if (mod && !fields) + fail(`'${inst.op}' names unknown module shape '${shapeImm}'`, inst); + if (isBornOp) { + // operand count must equal the shape's + // field count (+1 receiver for fill), at least one field — + // an empty born shape is a plain make_object, not this op. + const nvals = + inst.op === "make_object_shaped" + ? inst.operands.length + : inst.operands.length - 1; + if (fields && nvals !== fields.length) + fail( + `'${inst.op}' has ${nvals} values for shape '${shapeImm}' (${fields.length} fields)`, + inst + ); + if (nvals < 1) fail(`'${inst.op}' must install at least one field`, inst); + if (inst.op === "fill_object_shaped") { + // the receiver must be proven EMPTY-shaped here: the + // batched prefix is only equivalent to the sequential + // stores on an object with no fields yet (an un-killed + // has_shape(recv, "") fact — same engine as slot ops) + if (shapeFacts === undefined) shapeFacts = computeShapeFacts(fn); + const facts = shapeFacts ? shapeFacts.factsAt(b, i) : new Set(); + if (!facts.has(shapeFactKey(inst.operands[0]!.id, ""))) + fail( + `'fill_object_shaped' is not covered by an un-killed has_shape fact ` + + `for the empty shape on its receiver`, + inst + ); + } + return; + } + if (!isSlotOp) return; + + const slot = inst.imms["slot"]; + const repr = inst.imms["repr"]; + if (typeof slot !== "number" || slot < 0 || !Number.isInteger(slot)) + fail(`'${inst.op}' has a malformed slot immediate`, inst); + if (repr !== "boxed" && repr !== "f64") + fail(`'${inst.op}' has a malformed repr immediate`, inst); + if (inst.op === "slot_load") { + const want = repr === "f64" ? "f64" : "any"; + if (inst.type !== want) + fail(`slot_load repr "${String(repr)}" must have type ${want}, got ${inst.type}`, inst); + } + if (fields) { + if ((slot as number) >= fields.length) + fail( + `'${inst.op}' slot ${slot} out of bounds for shape '${shapeImm}' (${fields.length} fields)`, + inst + ); + if (fields[slot as number]!.repr !== repr) + fail( + `'${inst.op}' repr "${String(repr)}" != shape field repr "${fields[slot as number]!.repr}"`, + inst + ); + } + + if (shapeFacts === undefined) shapeFacts = computeShapeFacts(fn); + const facts = shapeFacts ? shapeFacts.factsAt(b, i) : new Set(); + if (!facts.has(shapeFactKey(inst.operands[0]!.id, shapeImm))) + fail( + `'${inst.op}' is not covered by an un-killed has_shape fact for shape '${shapeImm}'`, + inst + ); + + if (inst.op === "slot_store" && repr === "boxed") { + // the f64 case is the typed-flow rule above (raw f64 + // operand); boxed still needs the not-a-number proof + const val = inst.operands[1]!; + if (!tagFactDominates(val, false, b, idom)) + fail( + `slot_store lacks a dominating has_tag(number)=false fact ` + + `on its value for repr "boxed"`, + inst + ); + } + }); + } + + return true; +} + +export function verifyModule(mod: Module): boolean { + for (const fn of mod.functions) verifyFunction(fn, mod); + return true; +} diff --git a/lib/errors.js b/lib/errors.js deleted file mode 100644 index e4c978d1..00000000 --- a/lib/errors.js +++ /dev/null @@ -1,43 +0,0 @@ -/* -*- Mode: js2; indent-tabs-mode: nil; tab-width: 4; js2-indent-offset: 4; js2-basic-offset: 4; -*- - * vim: set ts=4 sw=4 et tw=99 ft=js: - */ - -class SourceError extends Error { - constructor(errorType, message, filename, loc = { start: { line: -1, column: -2 } }) { - super(message); - this.errorType = errorType; - this.message = message; - this.filename = filename; - this.loc = loc; - } - - reportToUser() { - throw this; - } - - toString() { - return `${this.filename}:${this.loc.start.line}:${this.loc.start.column + 1}: ${ - this.errorType - }: ${this.message}`; - } -} - -const ReportType = { - error: 0, - warn: 1, -}; - -function reportToUser(type, errorType, message, filename, loc) { - if (type === ReportType.error) throw new SourceError(errorType.name, message, filename, loc); - else if (loc && loc.start) - console.warn(`${filename}:${loc.start.line}:${loc.start.column + 1}: warning: ${message}`); - else console.warn(`${filename}:-1:-1: warning: ${message}`); -} - -export function reportError(errorType, message, filename, loc) { - reportToUser(ReportType.error, errorType, message, filename, loc); -} - -export function reportWarning(message, filename, loc) { - reportToUser(ReportType.warning, null, message, filename, loc); -} diff --git a/lib/errors.ts b/lib/errors.ts new file mode 100644 index 00000000..b0cdb7c2 --- /dev/null +++ b/lib/errors.ts @@ -0,0 +1,53 @@ +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: + */ + +import type { SourceLocation } from "./estree"; + +// callers pass an Error subclass constructor (TypeError, ReferenceError, +// ...) whose name labels the diagnostic +export type ErrorType = { name: string }; + +class SourceError extends Error { + errorType: string; + filename: string; + loc: SourceLocation; + + constructor( + errorType: string, + message: string, + filename: string, + loc: SourceLocation = { start: { line: -1, column: -2 } } + ) { + super(message); + this.errorType = errorType; + this.message = message; + this.filename = filename; + this.loc = loc; + } + + reportToUser(): never { + throw this; + } + + override toString(): string { + return `${this.filename}:${this.loc.start.line}:${this.loc.start.column + 1}: ${ + this.errorType + }: ${this.message}`; + } +} + +export function reportError( + errorType: ErrorType, + message: string, + filename: string, + loc?: SourceLocation +): never { + throw new SourceError(errorType.name, message, filename, loc); +} + +export function reportWarning(message: string, filename: string, loc?: SourceLocation): void { + if (loc && loc.start) + console.warn(`${filename}:${loc.start.line}:${loc.start.column + 1}: warning: ${message}`); + else console.warn(`${filename}:-1:-1: warning: ${message}`); +} diff --git a/lib/estree.ts b/lib/estree.ts new file mode 100644 index 00000000..7edc579d --- /dev/null +++ b/lib/estree.ts @@ -0,0 +1,563 @@ +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: + */ + +// The compiler's ESTree dialect: what the esprima fork produces, plus +// the properties our passes hang off the nodes. Dialect notes: +// - functions carry `defaults` (old-esprima parameter defaults) and +// may carry `rest`; +// - TryStatement has `handlers` (an array) and `guardedHandlers`; +// - CatchClause has a SpiderMonkey-era `guard`; +// - gather-imports adds `source_path` to import/export declarations; +// - EIR integration tags the toplevel function with eir_module / +// eir_main / ir_func and friends. + +import type { EjsFunction, DISubprogram } from "@llvm"; +import type { Module as EIRModule } from "./eir/ir"; + +export interface Position { + line: number; + column: number; +} + +export interface SourceLocation { + start: Position; + end?: Position; +} + +interface BaseNode { + loc?: SourceLocation | null; +} + +// --- expressions ------------------------------------------------------------ + +export interface ArrayExpression extends BaseNode { + type: "ArrayExpression"; + // elisions (holes) are null elements + elements: (Expression | SpreadElement | null)[]; +} + +export interface ObjectExpression extends BaseNode { + type: "ObjectExpression"; + properties: Property[]; +} + +export interface Property extends BaseNode { + type: "Property"; + key: Expression; + value: Expression | Pattern; + kind: "init" | "get" | "set"; + computed: boolean; + method?: boolean; + shorthand?: boolean; +} + +export interface Identifier extends BaseNode { + type: "Identifier"; + name: string; +} + +export interface Literal extends BaseNode { + type: "Literal"; + value: string | number | boolean | null | RegExp; + raw?: string; +} + +export interface TemplateLiteral extends BaseNode { + type: "TemplateLiteral"; + quasis: TemplateElement[]; + expressions: Expression[]; +} + +export interface TemplateElement extends BaseNode { + type: "TemplateElement"; + value: { cooked: string; raw: string }; + tail: boolean; +} + +export interface TaggedTemplateExpression extends BaseNode { + type: "TaggedTemplateExpression"; + tag: Expression; + quasi: TemplateLiteral; +} + +export interface FunctionBase extends BaseNode { + id: Identifier | null; + params: Pattern[]; + defaults: (Expression | null)[]; + rest?: Identifier | null; + body: BlockStatement | Expression; + generator: boolean; + expression: boolean; + // --- compiler extensions ------------------------------------------------- + // set by insert_toplevel_func on the synthetic module toplevel + toplevel?: boolean; + displayName?: string; + // set by collectEIRToplevel + eir_module?: EIRModule; + eir_main?: string; + // set by compile() for the toplevel wrapper + ir_name?: string; + ir_func?: EjsFunction & { debug_info?: DISubprogram }; +} + +export interface FunctionDeclaration extends FunctionBase { + type: "FunctionDeclaration"; + id: Identifier; + body: BlockStatement; +} + +export interface FunctionExpression extends FunctionBase { + type: "FunctionExpression"; + body: BlockStatement; +} + +export interface ArrowFunctionExpression extends FunctionBase { + type: "ArrowFunctionExpression"; +} + +export interface UnaryExpression extends BaseNode { + type: "UnaryExpression"; + operator: "-" | "+" | "!" | "~" | "typeof" | "void" | "delete"; + prefix?: boolean; + argument: Expression; +} + +export interface UpdateExpression extends BaseNode { + type: "UpdateExpression"; + operator: "++" | "--"; + argument: Expression; + prefix: boolean; +} + +export type BinaryOperator = + | "==" | "!=" | "===" | "!==" + | "<" | "<=" | ">" | ">=" + | "<<" | ">>" | ">>>" + | "+" | "-" | "*" | "/" | "%" + | "|" | "^" | "&" + | "in" | "instanceof"; + +export interface BinaryExpression extends BaseNode { + type: "BinaryExpression"; + operator: BinaryOperator; + left: Expression; + right: Expression; +} + +export type AssignmentOperator = + | "=" | "+=" | "-=" | "*=" | "/=" | "%=" + | "<<=" | ">>=" | ">>>=" | "|=" | "^=" | "&="; + +export interface AssignmentExpression extends BaseNode { + type: "AssignmentExpression"; + operator: AssignmentOperator; + left: Expression | Pattern; + right: Expression; +} + +export interface LogicalExpression extends BaseNode { + type: "LogicalExpression"; + operator: "||" | "&&"; + left: Expression; + right: Expression; +} + +export interface MemberExpression extends BaseNode { + type: "MemberExpression"; + object: Expression | Super; + property: Expression; + computed: boolean; +} + +export interface ConditionalExpression extends BaseNode { + type: "ConditionalExpression"; + test: Expression; + consequent: Expression; + alternate: Expression; +} + +export interface CallExpression extends BaseNode { + type: "CallExpression"; + callee: Expression | Super; + arguments: (Expression | SpreadElement)[]; +} + +export interface NewExpression extends BaseNode { + type: "NewExpression"; + callee: Expression; + arguments: (Expression | SpreadElement)[]; +} + +export interface SequenceExpression extends BaseNode { + type: "SequenceExpression"; + expressions: Expression[]; +} + +export interface SpreadElement extends BaseNode { + type: "SpreadElement"; + argument: Expression; +} + +export interface YieldExpression extends BaseNode { + type: "YieldExpression"; + argument: Expression | null; + delegate: boolean; +} + +export interface ThisExpression extends BaseNode { + type: "ThisExpression"; +} + +export interface Super extends BaseNode { + type: "Super"; +} + +export interface MetaProperty extends BaseNode { + type: "MetaProperty"; + // dialect: the esprima fork stores the raw NAMES here, not + // Identifier nodes + meta: string; + property: string; +} + +// --- patterns --------------------------------------------------------------- + +export interface ObjectPattern extends BaseNode { + type: "ObjectPattern"; + properties: Property[]; +} + +export interface ArrayPattern extends BaseNode { + type: "ArrayPattern"; + // dialect: declaration-position rests parse as SpreadElement, + // assignment-position ones as RestElement + elements: (Pattern | SpreadElement | null)[]; +} + +export interface RestElement extends BaseNode { + type: "RestElement"; + argument: Pattern; +} + +export interface AssignmentPattern extends BaseNode { + type: "AssignmentPattern"; + left: Pattern; + right: Expression; +} + +// --- statements ------------------------------------------------------------- + +export interface Program extends BaseNode { + type: "Program"; + body: Statement[]; + sourceType?: "script" | "module"; +} + +export interface ExpressionStatement extends BaseNode { + type: "ExpressionStatement"; + expression: Expression; +} + +export interface BlockStatement extends BaseNode { + type: "BlockStatement"; + body: Statement[]; +} + +export interface EmptyStatement extends BaseNode { + type: "EmptyStatement"; +} + +export interface DebuggerStatement extends BaseNode { + type: "DebuggerStatement"; +} + +export interface WithStatement extends BaseNode { + type: "WithStatement"; + object: Expression; + body: Statement; +} + +export interface ReturnStatement extends BaseNode { + type: "ReturnStatement"; + argument: Expression | null; +} + +export interface LabeledStatement extends BaseNode { + type: "LabeledStatement"; + label: Identifier; + body: Statement; +} + +export interface BreakStatement extends BaseNode { + type: "BreakStatement"; + label: Identifier | null; +} + +export interface ContinueStatement extends BaseNode { + type: "ContinueStatement"; + label: Identifier | null; +} + +export interface IfStatement extends BaseNode { + type: "IfStatement"; + test: Expression; + consequent: Statement; + alternate: Statement | null; +} + +export interface SwitchStatement extends BaseNode { + type: "SwitchStatement"; + discriminant: Expression; + cases: SwitchCase[]; +} + +export interface SwitchCase extends BaseNode { + type: "SwitchCase"; + test: Expression | null; + consequent: Statement[]; +} + +export interface ThrowStatement extends BaseNode { + type: "ThrowStatement"; + argument: Expression; +} + +export interface TryStatement extends BaseNode { + type: "TryStatement"; + block: BlockStatement; + handlers: CatchClause[]; + guardedHandlers: CatchClause[]; + finalizer: BlockStatement | null; +} + +export interface CatchClause extends BaseNode { + type: "CatchClause"; + param: Pattern; + guard: Expression | null; + body: BlockStatement; +} + +export interface WhileStatement extends BaseNode { + type: "WhileStatement"; + test: Expression; + body: Statement; +} + +export interface DoWhileStatement extends BaseNode { + type: "DoWhileStatement"; + body: Statement; + test: Expression; +} + +export interface ForStatement extends BaseNode { + type: "ForStatement"; + init: VariableDeclaration | Expression | null; + test: Expression | null; + update: Expression | null; + body: Statement; +} + +export interface ForInStatement extends BaseNode { + type: "ForInStatement"; + left: VariableDeclaration | Pattern; + right: Expression; + body: Statement; +} + +export interface ForOfStatement extends BaseNode { + type: "ForOfStatement"; + left: VariableDeclaration | Pattern; + right: Expression; + body: Statement; +} + +export interface VariableDeclaration extends BaseNode { + type: "VariableDeclaration"; + kind: "var" | "let" | "const"; + declarations: VariableDeclarator[]; +} + +export interface VariableDeclarator extends BaseNode { + type: "VariableDeclarator"; + id: Pattern; + init: Expression | null | undefined; +} + +// --- classes ---------------------------------------------------------------- + +export interface ClassBase extends BaseNode { + id: Identifier | null; + superClass: Expression | null; + body: ClassBody; +} + +export interface ClassDeclaration extends ClassBase { + type: "ClassDeclaration"; + id: Identifier; +} + +export interface ClassExpression extends ClassBase { + type: "ClassExpression"; +} + +export interface ClassBody extends BaseNode { + type: "ClassBody"; + body: MethodDefinition[]; +} + +export interface MethodDefinition extends BaseNode { + type: "MethodDefinition"; + key: Expression; + value: FunctionExpression; + kind: "init" | "constructor" | "method" | "get" | "set"; + computed?: boolean; + static?: boolean; +} + +// --- modules ---------------------------------------------------------------- + +export interface ModuleSpecifierBase extends BaseNode { + local: Identifier; +} + +export interface ImportSpecifier extends ModuleSpecifierBase { + type: "ImportSpecifier"; + imported: Identifier; + // legacy alias some paths still consult + id?: Identifier; +} + +export interface ImportDefaultSpecifier extends ModuleSpecifierBase { + type: "ImportDefaultSpecifier"; + id?: Identifier; +} + +export interface ImportNamespaceSpecifier extends ModuleSpecifierBase { + type: "ImportNamespaceSpecifier"; + id?: Identifier; +} + +export interface ImportDeclaration extends BaseNode { + type: "ImportDeclaration"; + specifiers: (ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier)[]; + source: Literal; + // added by gather-imports: the resolved module path literal + source_path?: Literal & { value: string }; +} + +export interface ExportSpecifier extends BaseNode { + type: "ExportSpecifier"; + local: Identifier; + exported: Identifier; +} + +export interface ExportNamedDeclaration extends BaseNode { + type: "ExportNamedDeclaration"; + declaration: Statement | null; + specifiers: ExportSpecifier[]; + source: Literal | null; + source_path?: Literal & { value: string }; +} + +export interface ExportDefaultDeclaration extends BaseNode { + type: "ExportDefaultDeclaration"; + declaration: Expression | FunctionDeclaration | ClassDeclaration | VariableDeclaration; +} + +export interface ExportAllDeclaration extends BaseNode { + type: "ExportAllDeclaration"; + source: Literal; + source_path?: Literal & { value: string }; +} + +// --- unions ----------------------------------------------------------------- + +export type Function = FunctionDeclaration | FunctionExpression | ArrowFunctionExpression; + +export type Class = ClassDeclaration | ClassExpression; + +export type ModuleDeclarationNode = + | ImportDeclaration + | ExportNamedDeclaration + | ExportDefaultDeclaration + | ExportAllDeclaration; + +export type Pattern = + | Identifier + | ObjectPattern + | ArrayPattern + | RestElement + | AssignmentPattern + | MemberExpression; // assignment-position targets + +export type Expression = + | ArrayExpression + | ObjectExpression + | Identifier + | Literal + | TemplateLiteral + | TaggedTemplateExpression + | FunctionExpression + | ArrowFunctionExpression + | UnaryExpression + | UpdateExpression + | BinaryExpression + | AssignmentExpression + | LogicalExpression + | MemberExpression + | ConditionalExpression + | CallExpression + | NewExpression + | SequenceExpression + | SpreadElement + | YieldExpression + | ThisExpression + | Super + | MetaProperty + | ClassExpression + | ObjectPattern + | ArrayPattern; + +export type Statement = + | ExpressionStatement + | BlockStatement + | EmptyStatement + | DebuggerStatement + | WithStatement + | ReturnStatement + | LabeledStatement + | BreakStatement + | ContinueStatement + | IfStatement + | SwitchStatement + | ThrowStatement + | TryStatement + | WhileStatement + | DoWhileStatement + | ForStatement + | ForInStatement + | ForOfStatement + | VariableDeclaration + | FunctionDeclaration + | ClassDeclaration + | ModuleDeclarationNode; + +export type Node = + | Program + | Statement + | Expression + | Pattern + | Property + | SwitchCase + | CatchClause + | VariableDeclarator + | TemplateElement + | ClassBody + | MethodDefinition + | ImportSpecifier + | ImportDefaultSpecifier + | ImportNamespaceSpecifier + | ExportSpecifier; + +export type NodeType = Node["type"]; diff --git a/lib/exitable-scope.js b/lib/exitable-scope.js deleted file mode 100644 index d503f59c..00000000 --- a/lib/exitable-scope.js +++ /dev/null @@ -1,181 +0,0 @@ -/* -*- Mode: js2; indent-tabs-mode: nil; tab-width: 4; js2-indent-offset: 4; js2-basic-offset: 4; -*- - * vim: set ts=4 sw=4 et tw=99 ft=js: - */ -import { Stack } from "./stack-es6"; - -import * as llvm from "@llvm"; -let irbuilder = llvm.IRBuilder; - -import * as consts from "./consts"; - -// -// ExitableScopes are basically the means by which ejs deals with 'break' and 'continue'. -// -// Each ExitableScope has two exit functions, exitFore and exitAft. -// exitFore corresponds to 'continue', and exitAft corresponds to -// 'break' (or falling off the end of the scope if the fromBreak arg is false.) -// -export class ExitableScope { - constructor(label = null) { - this.label = label; - this.parent = null; - } - - exitFore() { - throw new Error("Exitable scope does not allow exitFore"); - } - - exitAft() { - throw new Error("Exitable scope does not allow exitAft"); - } - - enter() { - this.parent = ExitableScope.scopeStack; - ExitableScope.scopeStack = this; - } - - leave() { - ExitableScope.scopeStack = this.parent; - this.parent = null; - } -} -ExitableScope.scopeStack = null; -ExitableScope.REASON_RETURN = -10; - -export class TryExitableScope extends ExitableScope { - constructor(cleanup_reason, cleanup_bb, create_landing_pad_bb, hasFinally) { - super(); - this.cleanup_reason = cleanup_reason; - this.cleanup_bb = cleanup_bb; - this.create_landing_pad_bb = create_landing_pad_bb; - this.hasFinally = hasFinally; - this.isTry = true; - this.destinations = []; - } - - enterTry() { - TryExitableScope.unwindStack.push(this); - } - - leaveTry() { - TryExitableScope.unwindStack.pop(); - } - - getLandingPadBlock() { - if (!this.landing_pad_block) this.landing_pad_block = this.create_landing_pad_bb(); - return this.landing_pad_block; - } - - lookupDestinationIdForScope(scope, reason) { - for (let dest of this.destinations) - if (dest.scope === scope && dest.reason === reason) return dest.id; - - let id = consts.int32(this.destinations.length); - this.destinations.unshift({ scope: scope, reason: reason, id: id }); - return id; - } - - exitFore(label = null) { - let scope; - if (label) scope = LoopExitableScope.findLabeledOrFinally(label, this.parent); - else scope = LoopExitableScope.findLoopOrFinally(this.parent); - - if (this.hasFinally) { - let reason = this.lookupDestinationIdForScope(scope, TryExitableScope.REASON_CONTINUE); - irbuilder.createStore(reason, this.cleanup_reason); - irbuilder.createBr(this.cleanup_bb); - } else { - scope.exitFore(); - } - } - - exitAft(fromBreak, label = null) { - let scope; - // first we find our destination scope - if (fromBreak) { - if (label) scope = LoopExitableScope.findLabeledOrFinally(label, this.parent); - else scope = this.parent; - } - - // then we either create a branch to our cleanup_bb - // with the right reason (we'll encode the exitAft from - // the dest scope in the cleanup_bb), or we exit from - // the dest scope directly if we're lacking a cleanup_bb - if (this.hasFinally) { - let reason; - if (fromBreak) - reason = this.lookupDestinationIdForScope(scope, TryExitableScope.REASON_BREAK); - else reason = consts.int32(TryExitableScope.REASON_FALLOFF_TRY); - - irbuilder.createStore(reason, this.cleanup_reason); - irbuilder.createBr(this.cleanup_bb); - } else { - if (fromBreak) scope.exitAft(fromBreak); - else irbuilder.createBr(this.cleanup_bb); - } - } -} -TryExitableScope.REASON_FALLOFF_TRY = -2; // we fell off the end of the try block -TryExitableScope.REASON_ERROR = -1; // error condition -TryExitableScope.REASON_BREAK = "break"; -TryExitableScope.REASON_CONTINUE = "continue"; -TryExitableScope.unwindStack = new Stack(); - -export class SwitchExitableScope extends ExitableScope { - constructor(merge_bb) { - super(); - this.merge_bb = merge_bb; - } - - exitAft() { - irbuilder.createBr(this.merge_bb); - } -} - -export class LoopExitableScope extends ExitableScope { - constructor(label, fore_bb, aft_bb) { - super(label); - this.fore_bb = fore_bb; - this.aft_bb = aft_bb; - this.isLoop = true; - } - - exitFore(label = null) { - if (label && label !== this.label) - LoopExitableScope.findLabeledOrFinally(label).exitFore(label); - else irbuilder.createBr(this.fore_bb); - } - - exitAft(fromBreak, label = null) { - if (label && label !== this.label) - LoopExitableScope.findLabeledOrFinally(label).exitAft(label); - else irbuilder.createBr(this.aft_bb); - } - - static findLabeledOrFinally(l, stack = ExitableScope.scopeStack) { - if (l === stack.label) return stack; - if (stack.hasFinally) return stack; - return LoopExitableScope.findLabeledOrFinally(l, stack.parent); - } - - static findLoopOrFinally(stack = ExitableScope.scopeStack) { - if (stack.isLoop) return stack; - if (stack.hasFinally) return stack; - return LoopExitableScope.findLoopOrFinally(stack.parent); - } -} - -export class LabeledStatementExitableScope extends ExitableScope { - constructor(label, aft_bb) { - super(label); - this.aft_bb = aft_bb; - } - - exitAft() { - irbuilder.createBr(this.aft_bb); - } - - exitFore() { - throw new Error("cannot continue this label"); - } -} diff --git a/lib/host-config.d.ts b/lib/host-config.d.ts new file mode 100644 index 00000000..69c3e294 --- /dev/null +++ b/lib/host-config.d.ts @@ -0,0 +1,8 @@ +// declarations for the GENERATED lib/host-config.js (see +// host-config.js.in and the //lib:host-config.js genrule) +export const LLVM_SUFFIX: string; +export const LLVM_BINDIR: string; +// major version of the LLVM the compiler was built against ("22"); +// the driver refuses to spawn a different major's opt/llc +export const LLVM_MAJOR: string; +export const RUNLOOP_IMPL: string; diff --git a/lib/host-config.js.in b/lib/host-config.js.in index c8bf019d..40833a6f 100644 --- a/lib/host-config.js.in +++ b/lib/host-config.js.in @@ -1,2 +1,4 @@ export let LLVM_SUFFIX = '@LLVM_SUFFIX@'; +export let LLVM_BINDIR = '@LLVM_BINDIR@'; +export let LLVM_MAJOR = '@LLVM_MAJOR@'; export let RUNLOOP_IMPL = '@RUNLOOP_IMPL@'; diff --git a/lib/llvm.d.ts b/lib/llvm.d.ts new file mode 100644 index 00000000..9a911c06 --- /dev/null +++ b/lib/llvm.d.ts @@ -0,0 +1,276 @@ +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: + */ + +// Ambient declarations for the "@llvm" native module (node-llvm when +// node-hosted, ejs-llvm when self-hosted). The surface here is exactly +// what the compiler uses — extend it as needs grow; do NOT widen types +// to any. +// +// The compiler also hangs bookkeeping properties off llvm objects +// (is_constant on constants, entry_bb/literalAllocas/topScope on +// functions, ...). Those are declared here, optional, so the habit is +// visible and type-checked rather than smuggled. + +declare module "@llvm" { + // --- values -------------------------------------------------------------- + + interface Value { + setName(name: string): void; + dump(): void; + // compiler bookkeeping: values that hold the runtime's boxed-bool + // encoding (see loadBoolEjsValue / forwardCalleeAttributes) + _ejs_returns_ejsval_bool?: boolean; + // compiler bookkeeping: constant tracking (see consts.ts) + is_constant?: boolean; + constant_val?: string | number | boolean | number[] | null; + } + + interface Constant extends Value {} + + const Constant: { + getNull(type: Type): Constant; + getAggregateZero(type: Type): Constant; + getIntegerValue(type: Type, ...val: number[]): Constant; + }; + + const ConstantFP: { + getDouble(val: number): Constant; + }; + + const ConstantArray: { + get(type: Type, elements: Constant[]): Constant; + }; + + // --- types --------------------------------------------------------------- + + interface Type { + pointerTo(): Type; + } + + interface StructType extends Type { + setStructBody(elements: Type[]): void; + } + + interface FunctionType extends Type {} + + const Type: { + getInt1Ty(): Type; + getInt8Ty(): Type; + getInt16Ty(): Type; + getInt32Ty(): Type; + getInt64Ty(): Type; + getDoubleTy(): Type; + getVoidTy(): Type; + }; + + const StructType: { + create(name: string, elements: Type[]): StructType; + }; + + const FunctionType: { + // the 3-arg form is jsllvm's sret shape: the real return value + // is written through an sret pointer while `ret` is void + get(ret: Type, params: Type[], sret?: Type): FunctionType; + }; + + const ArrayType: { + get(elem: Type, count: number): Type; + }; + + // --- functions / globals / blocks ----------------------------------------- + + interface Argument extends Value {} + + interface EjsFunction extends Value { + args: Argument[]; + argSize: number; + type: FunctionType; + returnType: Type; + setInternalLinkage(): void; + setExternalLinkage(): void; + setDoesNotThrow(): void; + setDoesNotAccessMemory(): void; + setOnlyReadsMemory(): void; + setStructRet(): void; + hasStructRetAttr(): boolean; + setGC(name: string): void; + setPersonality(fn: Value): void; + // compiler bookkeeping + doesNotThrow?: boolean; + doesNotAccessMemory?: boolean; + onlyReadsMemory?: boolean; + returns_ejsval_bool?: boolean; + takes_builtins?: boolean; + entry_bb?: BasicBlock; + literalAllocas?: Record; + topScope?: Map; + bits_alloca?: AllocaInst; + debug_info?: DISubprogram; + hasPersonality(): boolean; + } + + interface BasicBlock { + parent: EjsFunction; + } + const BasicBlock: { + new (name: string, parent: EjsFunction): BasicBlock; + }; + + interface GlobalVariable extends Value { + setInitializer(init: Constant): void; + setAlignment(align: number): void; + } + const GlobalVariable: { + new ( + module: Module, + type: Type, + name: string, + init: Constant | null, + visible?: boolean + ): GlobalVariable; + }; + + interface Module { + setTriple(triple: string): void; + setDataLayout(layout: string): void; + getOrInsertFunction(name: string, ret: Type, params: Type[]): EjsFunction; + getOrInsertExternalFunction(name: string, ret: Type, params: Type[]): EjsFunction; + getOrInsertGlobal(name: string, type: Type): GlobalVariable; + getOrInsertIntrinsic(name: string, types?: Type[]): EjsFunction; + getFunction(name: string): EjsFunction | null; + writeToFile(path: string): void; + writeBitcodeToFile(path: string): void; + dump(): void; + toString(): string; + } + const Module: { + new (name: string): Module; + }; + + // --- instruction building -------------------------------------------------- + + interface CallInst extends Value { + setOnlyReadsMemory(): void; + setDoesNotAccessMemory(): void; + setDoesNotThrow(): void; + setStructRet(): void; + } + + interface InvokeInst extends CallInst {} + + interface LandingPad extends Value { + setCleanup(cleanup: boolean): void; + addClause(clause: Value): void; + } + + interface PhiNode extends Value { + addIncoming(value: Value, block: BasicBlock): void; + } + + interface AllocaInst extends Value { + setAlignment(align: number): void; + } + + interface Switch extends Value { + addCase(val: Constant, dest: BasicBlock): void; + } + + const IRBuilder: { + setInsertPoint(bb: BasicBlock | null): void; + setInsertPointStartBB(bb: BasicBlock): void; + getInsertBlock(): BasicBlock | null; + setCurrentDebugLocation(loc: DebugLoc): void; + getCurrentDebugLocation(): DebugLoc; + + createAlloca(type: Type, name: string): AllocaInst; + createBitCast(value: Value, type: Type, name: string): Value; + createBr(bb: BasicBlock): Value; + createCondBr(cond: Value, then_bb: BasicBlock, else_bb: BasicBlock): Value; + createCall(fnType: FunctionType, callee: Value, args: Value[], name: string): CallInst; + createInvoke( + fnType: FunctionType, + callee: Value, + args: Value[], + normal: BasicBlock, + unwind: BasicBlock, + name: string + ): InvokeInst; + createExtractValue(agg: Value, idx: number, name: string): Value; + createGetElementPointer(type: Type, ptr: Value, idxs: Value[], name: string): Value; + createInBoundsGetElementPointer( + type: Type, + ptr: Value, + idxs: Value[], + name: string + ): Value; + createGlobalStringPtr(value: string, name: string): Constant; + createICmpEq(l: Value, r: Value, name: string): Value; + createFAdd(l: Value, r: Value, name: string): Value; + createFSub(l: Value, r: Value, name: string): Value; + createFMul(l: Value, r: Value, name: string): Value; + createFDiv(l: Value, r: Value, name: string): Value; + createFCmpOLT(l: Value, r: Value, name: string): Value; + createICmpSGt(l: Value, r: Value, name: string): Value; + createICmpUGE(l: Value, r: Value, name: string): Value; + createICmpUGt(l: Value, r: Value, name: string): Value; + createICmpULt(l: Value, r: Value, name: string): Value; + createAnd(l: Value, r: Value, name: string): Value; + createIntToPtr(value: Value, type: Type, name: string): Value; + createLandingPad(type: Type, numClauses: number, name: string): LandingPad; + createLoad(type: Type, ptr: Value, name: string): Value; + createNswSub(l: Value, r: Value, name: string): Value; + createOr(l: Value, r: Value, name: string): Value; + createPhi(type: Type, count: number, name: string): PhiNode; + createPointerCast(value: Value, type: Type, name: string): Value; + createPtrToInt(value: Value, type: Type, name: string): Value; + createRet(value: Value): Value; + createRetVoid(): Value; + createSelect(cond: Value, t: Value, f: Value, name: string): Value; + createStore(value: Value, ptr: Value, name?: string): Value; + createSwitch(value: Value, dflt: BasicBlock, numCases: number): Switch; + createTrunc(value: Value, type: Type, name: string): Value; + createUnreachable(): Value; + createZExt(value: Value, type: Type, name: string): Value; + }; + + // --- debug info ------------------------------------------------------------- + + interface DebugLoc {} + const DebugLoc: { + get(line: number, column: number, scope: DIDescriptor): DebugLoc; + }; + + interface DIDescriptor {} + interface DIFile extends DIDescriptor {} + interface DISubprogram extends DIDescriptor {} + + interface DIBuilder { + createFile(filename: string, directory: string): DIFile; + createCompileUnit( + filename: string, + directory: string, + producer: string, + optimized: boolean, + flags: string, + runtimeVersion: number + ): DIDescriptor; + createFunction( + scope: DIDescriptor, + name: string, + displayName: string, + file: DIFile, + lineNo: number, + isLocalToUnit: boolean, + isDefinition: boolean, + scopeLine: number, + flags: number, + isOptimized: boolean, + fn: EjsFunction + ): DISubprogram; + finalize(): void; + } + const DIBuilder: { + new (module: Module): DIBuilder; + }; +} diff --git a/lib/map.js b/lib/map.js deleted file mode 100644 index 02ce32c5..00000000 --- a/lib/map.js +++ /dev/null @@ -1,95 +0,0 @@ -(function () { - var echo_util = require("echo-util"); - var foldl = echo_util.foldl; - - var hasOwn = Object.prototype.hasOwnProperty; - - function Map() { - this.map = Object.create(null); - this.map_size = 0; - } - - Map.prototype.has = function (key) { - return hasOwn.call(this.map, "%map" + JSON.stringify(key)); - }; - - Map.prototype.set = function (key, val) { - var entry_key = "%map" + JSON.stringify(key); - var had_before = hasOwn.call(this.map, entry_key); - this.map[entry_key] = { key: key, val: val }; - if (!had_before) this.map_size++; - }; - - Map.prototype.get = function (key, val) { - var entry_key = "%map" + JSON.stringify(key); - if (!hasOwn.call(this.map, entry_key)) return undefined; - return this.map[entry_key].val; - }; - - Map.prototype.remove = function (key) { - var entry_key = "%map" + JSON.stringify(key); - var had_before = hasOwn.call(this.map, entry_key); - - delete this.map[entry_key]; - if (had_before) this.map_size--; - }; - - Map.prototype.clear = function () { - this.map = Object.create(null); - this.map_size = 0; - }; - - Map.prototype.forEach = function (f) { - for (var p in this.map) { - if (!hasOwn.call(this.map, p)) continue; - var entry = this.map[p]; - f(entry.val, entry.key, this); - } - }; - - Map.prototype.keys = function () { - var result = []; - for (var p in this.map) { - if (!hasOwn.call(this.map, p)) continue; - var entry = this.map[p]; - result.push(entry.key); - } - return result; - }; - Map.prototype.values = function () { - var result = []; - for (var p in this.map) { - if (!hasOwn.call(this.map, p)) continue; - var entry = this.map[p]; - result.push(entry.value); - } - return result; - }; - - Map.prototype.size = function () { - return this.map_size; - }; - - exports.Map = Map; -})(); - -/* -# Set tests - -s1 = new Set [1, 2, 3, 4] -s2 = new Set [5, 6, 7, 8] - -console.log "should be { 1 2 3 4 5 6 7 8 }: #{(s1.union s2).toString()}" - - -s3 = new Set [1, 2, 3, 4, 5, 6, 7, 8] -s4 = new Set [5, 6, 7, 8] - -console.log "should be { 1 2 3 4 }: #{(s3.subtract s4).toString()}" - -s5 = new Set [1, 2, 3, 4]; -s6 = new Set [3, 4, 5]; - -console.log "should be { 3 4 }: #{(s5.intersect s6).toString()}" - -*/ diff --git a/lib/module-info.js b/lib/module-info.js deleted file mode 100644 index 2f0ec467..00000000 --- a/lib/module-info.js +++ /dev/null @@ -1,107 +0,0 @@ -/* -*- Mode: js2; indent-tabs-mode: nil; tab-width: 4; js2-indent-offset: 4; js2-basic-offset: 4; -*- - * vim: set ts=4 sw=4 et tw=99 ft=js: - */ - -import { intrinsic, sanitize_with_regexp } from "./echo-util"; -import { moduleGetSlot_id, moduleSetSlot_id, env_unused_id, value_id } from "./common-ids"; -import * as b from "./ast-builder"; - -export class ModuleInfo { - constructor(is_native) { - this.slot_num = 0; - this.exports = new Map(); - this.importList = []; - this.has_default = false; - this.is_native = is_native; - } - - setHasDefaultExport() { - this.has_default = true; - } - - hasDefaultExport() { - return this.has_default; - } - - addExport(ident, constval) { - this.exports.set(ident, { - constval: constval, - slot_num: this.slot_num, - }); - this.slot_num++; - } - - addImportSource(source_path) { - if (this.importList.indexOf(source_path) === -1) this.importList.push(source_path); - } - - isNative() { - return this.is_native; - } -} - -export class JSModuleInfo extends ModuleInfo { - constructor(path) { - super(false); - this.path = path; - let sanitized_path = sanitize_with_regexp(path); - this.toplevel_function_name = `_ejs_toplevel_${sanitized_path}`; - this.module_name = `_ejs_module_${sanitized_path}`; - } - - getExportGetter(ident) { - let export_info = this.exports.get(ident); - let function_id = b.identifier(`get_export_${ident}`); - let loc = { start: { line: 0, column: 0 } }; - if (export_info.constval) { - return b.functionExpression( - function_id, - [env_unused_id], - b.blockStatement([b.returnStatement(export_info.constval)], loc), - [], - null, - loc - ); - } else { - return b.functionExpression( - function_id, - [env_unused_id], - b.blockStatement( - [ - b.returnStatement( - intrinsic(moduleGetSlot_id, [b.literal(this.path), b.literal(ident)]) - ), - ], - loc - ), - [], - null, - loc - ); - } - } - - getExportSetter(ident) { - let function_id = b.identifier(`set_export_${ident}`); - // we shouldn't generate a setter for const exports - return b.functionExpression( - function_id, - [env_unused_id, value_id], - b.blockStatement([ - intrinsic(moduleSetSlot_id, [b.literal(this.path), b.literal(ident), value_id]), - ]) - ); - } -} - -export class NativeModuleInfo extends ModuleInfo { - constructor(name, init_function, link_flags, module_files, ejs_dir) { - super(true); - this.path = name; - this.module_name = name; - this.init_function = init_function; - this.link_flags = link_flags.join(" "); - this.module_files = module_files; - this.ejs_dir = ejs_dir; - } -} diff --git a/lib/module-info.ts b/lib/module-info.ts new file mode 100644 index 00000000..2b1aed63 --- /dev/null +++ b/lib/module-info.ts @@ -0,0 +1,109 @@ +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: + */ + +import { sanitize_with_regexp } from "./echo-util"; +import type { Literal } from "./estree"; + +export interface ExportInfo { + // a const-literal export's folded initializer, when it has one + constval: Literal | undefined; + slot_num: number; + // a hidden slot for a non-exported module-level var (see + // addPromotedSlot); import resolution and the module-object + // accessors skip promoted entries + promoted?: boolean; +} + +export abstract class ModuleInfo { + slot_num = 0; + exports = new Map(); + importList: string[] = []; + has_default = false; + is_native: boolean; + + // every ModuleInfo names a module and its generated artifacts + abstract path: string; + abstract module_name: string; + + constructor(is_native: boolean) { + this.is_native = is_native; + } + + setHasDefaultExport(): void { + this.has_default = true; + } + + hasDefaultExport(): boolean { + return this.has_default; + } + + addExport(ident: string, constval?: Literal): void { + this.exports.set(ident, { + constval: constval, + slot_num: this.slot_num, + }); + this.slot_num++; + } + + // a hidden slot for a non-exported module-level var: it shares the + // export slot array (so allocation sizing and GC scanning need no + // changes) but is private to the module -- import resolution and the + // module-object accessors skip promoted entries. + addPromotedSlot(ident: string): void { + if (this.exports.has(ident)) return; + this.exports.set(ident, { + constval: undefined, + slot_num: this.slot_num, + promoted: true, + }); + this.slot_num++; + } + + addImportSource(source_path: string): void { + if (this.importList.indexOf(source_path) === -1) this.importList.push(source_path); + } + + isNative(): boolean { + return this.is_native; + } +} + +export class JSModuleInfo extends ModuleInfo { + path: string; + module_name: string; + toplevel_function_name: string; + + constructor(path: string) { + super(false); + this.path = path; + const sanitized_path = sanitize_with_regexp(path); + this.toplevel_function_name = `_ejs_toplevel_${sanitized_path}`; + this.module_name = `_ejs_module_${sanitized_path}`; + } +} + +export class NativeModuleInfo extends ModuleInfo { + path: string; + module_name: string; + init_function: string; + link_flags: string; + module_files: string[]; + ejs_dir: string; + + constructor( + name: string, + init_function: string, + link_flags: string[], + module_files: string[], + ejs_dir: string + ) { + super(true); + this.path = name; + this.module_name = name; + this.init_function = init_function; + this.link_flags = link_flags.join(" "); + this.module_files = module_files; + this.ejs_dir = ejs_dir; + } +} diff --git a/lib/node-compat.d.ts b/lib/node-compat.d.ts new file mode 100644 index 00000000..67f44ecd --- /dev/null +++ b/lib/node-compat.d.ts @@ -0,0 +1,24 @@ +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: + */ + +// The "@node-compat/*" modules resolve to node's own os/path/fs/... when +// node-hosted (the babel step rewrites the specifier) and to the +// node-compat native module when self-hosted. Their surface is node's. + +declare module "@node-compat/os" { + const os: typeof import("os"); + export = os; +} +declare module "@node-compat/path" { + const path: typeof import("path"); + export = path; +} +declare module "@node-compat/fs" { + const fs: typeof import("fs"); + export = fs; +} +declare module "@node-compat/child_process" { + const child_process: typeof import("child_process"); + export = child_process; +} diff --git a/lib/node-visitor.js b/lib/node-visitor.js deleted file mode 100644 index 9197a675..00000000 --- a/lib/node-visitor.js +++ /dev/null @@ -1,588 +0,0 @@ -/* -*- Mode: js2; indent-tabs-mode: nil; tab-width: 4; js2-indent-offset: 4; js2-basic-offset: 4; -*- - * vim: set ts=4 sw=4 et tw=99 ft=js: - */ - -import * as b from "./ast-builder"; - -export class TreeVisitor { - visitArrayKeep(arr, ...args) { - return arr.map((el) => this.visit(el, ...args)); - } - - visitArray(arr, ...args) { - let i = 0; - let e = arr.length; - - while (i < e) { - let tmp = this.visit(arr[i], ...args); - if (!tmp) { - arr.splice(i, 1); - e = arr.length; - } else if (Array.isArray(tmp)) { - let tmplen = tmp.length; - if (tmplen > 0) { - tmp.unshift(1); - tmp.unshift(i); - arr.splice.apply(arr, tmp); - i += tmplen; - e = arr.length; - } else { - arr.splice(i, 1); - e = arr.length; - } - } else { - arr[i] = tmp; - i += 1; - } - } - return arr; - } - - visit(n, ...args) { - if (!n) return n; - if (Array.isArray(n)) return this.visitArray(n, ...args); - - let rv = null; - switch (n.type) { - case b.ArrayExpression: - rv = this.visitArrayExpression(n, ...args); - break; - case b.ArrayPattern: - rv = this.visitArrayPattern(n, ...args); - break; - case b.ArrowFunctionExpression: - rv = this.visitArrowFunctionExpression(n, ...args); - break; - case b.AssignmentExpression: - rv = this.visitAssignmentExpression(n, ...args); - break; - case b.BinaryExpression: - rv = this.visitBinaryExpression(n, ...args); - break; - case b.BlockStatement: - rv = this.visitBlock(n, ...args); - break; - case b.BreakStatement: - rv = this.visitBreak(n, ...args); - break; - case b.CallExpression: - rv = this.visitCallExpression(n, ...args); - break; - case b.CatchClause: - rv = this.visitCatchClause(n, ...args); - break; - case b.ClassBody: - rv = this.visitClassBody(n, ...args); - break; - case b.ClassDeclaration: - rv = this.visitClassDeclaration(n, ...args); - break; - case b.ClassExpression: - rv = this.visitClassExpression(n, ...args); - break; - case b.ClassHeritage: - throw new Error(`Unhandled AST node type: ${n.type}, ${JSON.stringify(n)}`); - case b.ComprehensionBlock: - throw new Error(`Unhandled AST node type: ${n.type}, ${JSON.stringify(n)}`); - case b.ComprehensionExpression: - throw new Error(`Unhandled AST node type: ${n.type}, ${JSON.stringify(n)}`); - case b.ConditionalExpression: - rv = this.visitConditionalExpression(n, ...args); - break; - case b.ContinueStatement: - rv = this.visitContinue(n, ...args); - break; - case b.DebuggerStatement: - throw new Error(`Unhandled AST node type: ${n.type}, ${JSON.stringify(n)}`); - case b.DoWhileStatement: - rv = this.visitDo(n, ...args); - break; - case b.EmptyStatement: - rv = this.visitEmptyStatement(n, ...args); - break; - case b.ExportNamedDeclaration: - rv = this.visitExportNamedDeclaration(n, ...args); - break; // XXX jquery esprima - case b.ExportAllDeclaration: - rv = this.visitExportAllDeclaration(n, ...args); - break; // XXX jquery esprima - case b.ExportDefaultDeclaration: - rv = this.visitExportDefaultDeclaration(n, ...args); - break; // XXX jquery esprima - case b.ExpressionStatement: - rv = this.visitExpressionStatement(n, ...args); - break; - case b.ForInStatement: - rv = this.visitForIn(n, ...args); - break; - case b.ForOfStatement: - rv = this.visitForOf(n, ...args); - break; - case b.ForStatement: - rv = this.visitFor(n, ...args); - break; - case b.FunctionDeclaration: - rv = this.visitFunctionDeclaration(n, ...args); - break; - case b.FunctionExpression: - rv = this.visitFunctionExpression(n, ...args); - break; - case b.Identifier: - rv = this.visitIdentifier(n, ...args); - break; - case b.IfStatement: - rv = this.visitIf(n, ...args); - break; - case b.ImportDeclaration: - rv = this.visitImportDeclaration(n, ...args); - break; - case b.ImportSpecifier: - rv = this.visitImportSpecifier(n, ...args); - break; - case b.LabeledStatement: - rv = this.visitLabeledStatement(n, ...args); - break; - case b.Literal: - rv = this.visitLiteral(n, ...args); - break; - case b.LogicalExpression: - rv = this.visitLogicalExpression(n, ...args); - break; - case b.MemberExpression: - rv = this.visitMemberExpression(n, ...args); - break; - case b.MetaProperty: - rv = this.visitMetaProperty(n, ...args); - break; - case b.MethodDefinition: - rv = this.visitMethodDefinition(n, ...args); - break; - case b.ModuleDeclaration: - rv = this.visitModuleDeclaration(n, ...args); - break; - case b.NewExpression: - rv = this.visitNewExpression(n, ...args); - break; - case b.ObjectExpression: - rv = this.visitObjectExpression(n, ...args); - break; - case b.ObjectPattern: - rv = this.visitObjectPattern(n, ...args); - break; - case b.Program: - rv = this.visitProgram(n, ...args); - break; - case b.Property: - rv = this.visitProperty(n, ...args); - break; - case b.RestElement: - rv = this.visitRestElement(n, ...args); - break; - case b.ReturnStatement: - rv = this.visitReturn(n, ...args); - break; - case b.SequenceExpression: - rv = this.visitSequenceExpression(n, ...args); - break; - case b.SpreadElement: - rv = this.visitSpreadElement(n, ...args); - break; - case b.Super: - rv = this.visitSuper(n, ...args); - break; - case b.SwitchCase: - rv = this.visitCase(n, ...args); - break; - case b.SwitchStatement: - rv = this.visitSwitch(n, ...args); - break; - case b.TaggedTemplateExpression: - rv = this.visitTaggedTemplateExpression(n, ...args); - break; - case b.TemplateElement: - rv = this.visitTemplateElement(n, ...args); - break; - case b.TemplateLiteral: - rv = this.visitTemplateLiteral(n, ...args); - break; - case b.ThisExpression: - rv = this.visitThisExpression(n, ...args); - break; - case b.ThrowStatement: - rv = this.visitThrow(n, ...args); - break; - case b.TryStatement: - rv = this.visitTry(n, ...args); - break; - case b.UnaryExpression: - rv = this.visitUnaryExpression(n, ...args); - break; - case b.UpdateExpression: - rv = this.visitUpdateExpression(n, ...args); - break; - case b.VariableDeclaration: - rv = this.visitVariableDeclaration(n, ...args); - break; - case b.VariableDeclarator: - rv = this.visitVariableDeclarator(n, ...args); - break; - case b.WhileStatement: - rv = this.visitWhile(n, ...args); - break; - case b.WithStatement: - rv = this.visitWith(n, ...args); - break; - case b.YieldExpression: - rv = this.visitYield(n, ...args); - break; - default: - throw new Error(`PANIC: unknown parse node type ${n.type}, ${JSON.stringify(n)}`); - } - - if (rv == null) return n; - return rv; - } - - visitProgram(n, ...args) { - n.body = this.visitArray(n.body, ...args); - return n; - } - - visitFunction(n, ...args) { - n.params = this.visitArray(n.params, ...args); - n.body = this.visit(n.body, ...args); - return n; - } - - visitFunctionDeclaration(n, ...args) { - return this.visitFunction(n, ...args); - } - - visitFunctionExpression(n, ...args) { - return this.visitFunction(n, ...args); - } - - visitArrowFunctionExpression(n, ...args) { - return this.visitFunction(n, ...args); - } - - visitBlock(n, ...args) { - n.body = this.visitArray(n.body, ...args); - return n; - } - - visitEmptyStatement(n) { - return n; - } - - visitExpressionStatement(n, ...args) { - n.expression = this.visit(n.expression, ...args); - return n; - } - - visitSwitch(n, ...args) { - n.discriminant = this.visit(n.discriminant, ...args); - n.cases = this.visitArray(n.cases, ...args); - return n; - } - - visitCase(n, ...args) { - n.test = this.visit(n.test, ...args); - n.consequent = this.visit(n.consequent, ...args); - return n; - } - - visitFor(n, ...args) { - n.init = this.visit(n.init, ...args); - n.test = this.visit(n.test, ...args); - n.update = this.visit(n.update, ...args); - n.body = this.visit(n.body, ...args); - return n; - } - - visitWhile(n, ...args) { - n.test = this.visit(n.test, ...args); - n.body = this.visit(n.body, ...args); - return n; - } - - visitIf(n, ...args) { - n.test = this.visit(n.test, ...args); - n.consequent = this.visit(n.consequent, ...args); - n.alternate = this.visit(n.alternate, ...args); - return n; - } - - visitForIn(n, ...args) { - n.left = this.visit(n.left, ...args); - n.right = this.visit(n.right, ...args); - n.body = this.visit(n.body, ...args); - return n; - } - - visitForOf(n, ...args) { - n.left = this.visit(n.left, ...args); - n.right = this.visit(n.right, ...args); - n.body = this.visit(n.body, ...args); - return n; - } - - visitDo(n, ...args) { - n.body = this.visit(n.body, ...args); - n.test = this.visit(n.test, ...args); - return n; - } - - visitIdentifier(n) { - return n; - } - visitLiteral(n) { - return n; - } - visitThisExpression(n) { - return n; - } - visitBreak(n) { - return n; - } - visitContinue(n) { - return n; - } - - visitTry(n, ...args) { - n.block = this.visit(n.block, ...args); - if (n.handlers) n.handlers = this.visit(n.handlers, ...args); - else n.handlers = null; - n.finalizer = this.visit(n.finalizer, ...args); - return n; - } - - visitCatchClause(n, ...args) { - n.param = this.visit(n.param, ...args); - n.guard = this.visit(n.guard, ...args); - n.body = this.visit(n.body, ...args); - return n; - } - - visitThrow(n, ...args) { - n.argument = this.visit(n.argument, ...args); - return n; - } - - visitRestElement(n) { - return n; - } - - visitReturn(n, ...args) { - n.argument = this.visit(n.argument, ...args); - return n; - } - - visitWith(n, ...args) { - n.object = this.visit(n.object, ...args); - n.body = this.visit(n.body, ...args); - return n; - } - - visitYield(n, ...args) { - n.argument = this.visit(n.argument, ...args); - return n; - } - - visitVariableDeclaration(n, ...args) { - n.declarations = this.visitArray(n.declarations, ...args); - return n; - } - - visitVariableDeclarator(n, ...args) { - n.id = this.visit(n.id, ...args); - n.init = this.visit(n.init, ...args); - return n; - } - - visitLabeledStatement(n, ...args) { - n.label = this.visit(n.label, ...args); - n.body = this.visit(n.body, ...args); - return n; - } - - visitAssignmentExpression(n, ...args) { - n.left = this.visit(n.left, ...args); - n.right = this.visit(n.right, ...args); - return n; - } - - visitConditionalExpression(n, ...args) { - n.test = this.visit(n.test, ...args); - n.consequent = this.visit(n.consequent, ...args); - n.alternate = this.visit(n.alternate, ...args); - return n; - } - - visitLogicalExpression(n, ...args) { - n.left = this.visit(n.left, ...args); - n.right = this.visit(n.right, ...args); - return n; - } - - visitBinaryExpression(n, ...args) { - n.left = this.visit(n.left, ...args); - n.right = this.visit(n.right, ...args); - return n; - } - - visitUnaryExpression(n, ...args) { - n.argument = this.visit(n.argument, ...args); - return n; - } - - visitUpdateExpression(n, ...args) { - n.argument = this.visit(n.argument, ...args); - return n; - } - - visitMemberExpression(n, ...args) { - n.object = this.visit(n.object, ...args); - if (n.computed) n.property = this.visit(n.property, ...args); - return n; - } - - visitSequenceExpression(n, ...args) { - n.expressions = this.visitArray(n.expressions, ...args); - return n; - } - - visitSuper(n) { - return n; - } - - visitSpreadElement(n, ...args) { - n.arguments = this.visit(n.argument, ...args); - return n; - } - - visitNewExpression(n, ...args) { - n.callee = this.visit(n.callee, ...args); - n.arguments = this.visitArray(n.arguments, ...args); - return n; - } - - visitObjectExpression(n, ...args) { - n.properties = this.visitArray(n.properties, ...args); - return n; - } - - visitArrayExpression(n, ...args) { - // esprima encodes holes in the array as 'null' elements in - // n.elements, so we can't use visitArray. instead iterate - // over the elements manually. - n.elements = this.visitArrayKeep(n.elements, ...args); - return n; - } - - visitProperty(n, ...args) { - n.key = this.visit(n.key, ...args); - n.value = this.visit(n.value, ...args); - return n; - } - - visitCallExpression(n, ...args) { - n.callee = this.visit(n.callee, ...args); - n.arguments = this.visitArray(n.arguments, ...args); - return n; - } - - visitClassDeclaration(n, ...args) { - return this.visitClass(n, ...args); - } - - visitClassExpression(n, ...args) { - return this.visitClass(n, ...args); - } - - visitClass(n, ...args) { - n.body = this.visit(n.body, ...args); - return n; - } - - visitClassBody(n, ...args) { - n.body = this.visitArray(n.body, ...args); - return n; - } - - visitMetaProperty(n) { - return n; - } - - visitMethodDefinition(n, ...args) { - n.value = this.visit(n.value, ...args); - return n; - } - - visitModuleDeclaration(n, ...args) { - n.id = this.visit(n.id, ...args); - n.body = this.visit(n.body, ...args); - return n; - } - - visitExportDefaultDeclaration(n, ...args) { - n.declaration = this.visit(n.declaration, ...args); - return n; - } - - visitExportNamedDeclaration(n, ...args) { - n.declaration = this.visit(n.declaration, ...args); - // XXX specifiers? - return n; - } - - visitExportAllDeclaration(n) { - return n; - } - - visitImportDeclaration(n, ...args) { - n.specifiers = this.visitArray(n.specifiers, ...args); - return n; - } - - visitImportSpecifier(n, ...args) { - n.imported = this.visit(n.imported, ...args); - return n; - } - - visitArrayPattern(n, ...args) { - n.elements = this.visitArrayKeep(n.elements, ...args); - return n; - } - - visitObjectPattern(n, ...args) { - n.properties = this.visitArray(n.properties, ...args); - return n; - } - - visitTaggedTemplateExpression(n, ...args) { - n.quasi = this.visit(n.quasi, ...args); - return n; - } - - visitTemplateLiteral(n, ...args) { - n.quasis = this.visitArray(n.quasis, ...args); - n.expressions = this.visitArray(n.expressions, ...args); - return n; - } - - visitTemplateElement(n) { - return n; - } - - toString() { - return "TreeVisitor"; - } -} - -export class TransformPass extends TreeVisitor { - constructor(options) { - super(); - this.options = options; - } -} diff --git a/lib/node-visitor.ts b/lib/node-visitor.ts new file mode 100644 index 00000000..964fe494 --- /dev/null +++ b/lib/node-visitor.ts @@ -0,0 +1,632 @@ +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: + */ + +// The AST walker/transformer base. visit() dispatches on node type to a +// per-type method; a method returning null/undefined keeps the original +// node, returning a node replaces it, and (inside statement/expression +// lists) returning an array splices. +// +// Transformers must preserve the syntactic category of the slot they +// return into (an expression position must get back an expression, ...). +// That contract is asserted in exactly one place — visitAs — rather than +// scattered casts; violations surface downstream in lowering, which +// whitelists what it understands. + +import * as b from "./ast-builder"; +import type * as e from "./estree"; +import type { CompilerOptions } from "./options"; + +export type VisitResult = e.Node | e.Node[] | null | undefined; + +export class TreeVisitor { + // the category-preserving cast (see the module comment) + protected visitAs(n: T | null | undefined): T { + return this.visit(n) as T; + } + + protected visitNullable(n: T | null): T | null { + if (!n) return n; + return this.visit(n) as T; + } + + visitArrayKeep(arr: (T | null)[]): (T | null)[] { + return arr.map((el) => (el === null ? null : (this.visit(el) as T))); + } + + // in-place transform of a node list: a falsy result removes the + // element, an array result splices its elements in + visitArray(arr: T[]): T[] { + let i = 0; + let end = arr.length; + + while (i < end) { + const tmp = this.visit(arr[i]) as T | T[] | null | undefined; + if (!tmp) { + arr.splice(i, 1); + end = arr.length; + } else if (Array.isArray(tmp)) { + arr.splice(i, 1, ...tmp); + i += tmp.length; + end = arr.length; + } else { + arr[i] = tmp; + i += 1; + } + } + return arr; + } + + visit(n: e.Node | e.Node[] | null | undefined): VisitResult { + if (!n) return n; + if (Array.isArray(n)) return this.visitArray(n); + + let rv: VisitResult = null; + switch (n.type) { + case "ArrayExpression": + rv = this.visitArrayExpression(n); + break; + case "ArrayPattern": + rv = this.visitArrayPattern(n); + break; + case "ArrowFunctionExpression": + rv = this.visitArrowFunctionExpression(n); + break; + case "AssignmentExpression": + rv = this.visitAssignmentExpression(n); + break; + case "AssignmentPattern": + rv = this.visitAssignmentPattern(n); + break; + case "BinaryExpression": + rv = this.visitBinaryExpression(n); + break; + case "BlockStatement": + rv = this.visitBlock(n); + break; + case "BreakStatement": + rv = this.visitBreak(n); + break; + case "CallExpression": + rv = this.visitCallExpression(n); + break; + case "CatchClause": + rv = this.visitCatchClause(n); + break; + case "ClassBody": + rv = this.visitClassBody(n); + break; + case "ClassDeclaration": + rv = this.visitClassDeclaration(n); + break; + case "ClassExpression": + rv = this.visitClassExpression(n); + break; + case "ConditionalExpression": + rv = this.visitConditionalExpression(n); + break; + case "ContinueStatement": + rv = this.visitContinue(n); + break; + case "DebuggerStatement": + rv = n; // compiled as a no-op + break; + case "DoWhileStatement": + rv = this.visitDo(n); + break; + case "EmptyStatement": + rv = this.visitEmptyStatement(n); + break; + case "ExportNamedDeclaration": + rv = this.visitExportNamedDeclaration(n); + break; + case "ExportAllDeclaration": + rv = this.visitExportAllDeclaration(n); + break; + case "ExportDefaultDeclaration": + rv = this.visitExportDefaultDeclaration(n); + break; + case "ExportSpecifier": + rv = this.visitExportSpecifier(n); + break; + case "ExpressionStatement": + rv = this.visitExpressionStatement(n); + break; + case "ForInStatement": + rv = this.visitForIn(n); + break; + case "ForOfStatement": + rv = this.visitForOf(n); + break; + case "ForStatement": + rv = this.visitFor(n); + break; + case "FunctionDeclaration": + rv = this.visitFunctionDeclaration(n); + break; + case "FunctionExpression": + rv = this.visitFunctionExpression(n); + break; + case "Identifier": + rv = this.visitIdentifier(n); + break; + case "IfStatement": + rv = this.visitIf(n); + break; + case "ImportDeclaration": + rv = this.visitImportDeclaration(n); + break; + case "ImportSpecifier": + rv = this.visitImportSpecifier(n); + break; + case "ImportDefaultSpecifier": + rv = this.visitImportDefaultSpecifier(n); + break; + case "ImportNamespaceSpecifier": + rv = this.visitImportNamespaceSpecifier(n); + break; + case "LabeledStatement": + rv = this.visitLabeledStatement(n); + break; + case "Literal": + rv = this.visitLiteral(n); + break; + case "LogicalExpression": + rv = this.visitLogicalExpression(n); + break; + case "MemberExpression": + rv = this.visitMemberExpression(n); + break; + case "MetaProperty": + rv = this.visitMetaProperty(n); + break; + case "MethodDefinition": + rv = this.visitMethodDefinition(n); + break; + case "NewExpression": + rv = this.visitNewExpression(n); + break; + case "ObjectExpression": + rv = this.visitObjectExpression(n); + break; + case "ObjectPattern": + rv = this.visitObjectPattern(n); + break; + case "Program": + rv = this.visitProgram(n); + break; + case "Property": + rv = this.visitProperty(n); + break; + case "RestElement": + rv = this.visitRestElement(n); + break; + case "ReturnStatement": + rv = this.visitReturn(n); + break; + case "SequenceExpression": + rv = this.visitSequenceExpression(n); + break; + case "SpreadElement": + rv = this.visitSpreadElement(n); + break; + case "Super": + rv = this.visitSuper(n); + break; + case "SwitchCase": + rv = this.visitCase(n); + break; + case "SwitchStatement": + rv = this.visitSwitch(n); + break; + case "TaggedTemplateExpression": + rv = this.visitTaggedTemplateExpression(n); + break; + case "TemplateElement": + rv = this.visitTemplateElement(n); + break; + case "TemplateLiteral": + rv = this.visitTemplateLiteral(n); + break; + case "ThisExpression": + rv = this.visitThisExpression(n); + break; + case "ThrowStatement": + rv = this.visitThrow(n); + break; + case "TryStatement": + rv = this.visitTry(n); + break; + case "UnaryExpression": + rv = this.visitUnaryExpression(n); + break; + case "UpdateExpression": + rv = this.visitUpdateExpression(n); + break; + case "VariableDeclaration": + rv = this.visitVariableDeclaration(n); + break; + case "VariableDeclarator": + rv = this.visitVariableDeclarator(n); + break; + case "WhileStatement": + rv = this.visitWhile(n); + break; + case "WithStatement": + rv = this.visitWith(n); + break; + case "YieldExpression": + rv = this.visitYield(n); + break; + default: + throw new Error( + `PANIC: unknown parse node type ${(n as e.Node).type}, ${JSON.stringify(n)}` + ); + } + + if (rv == null) return n; + return rv; + } + + visitProgram(n: e.Program): VisitResult { + n.body = this.visitArray(n.body); + return n; + } + + visitFunction(n: e.Function): VisitResult { + n.params = this.visitArray(n.params); + n.body = this.visitAs(n.body); + return n; + } + + visitFunctionDeclaration(n: e.FunctionDeclaration): VisitResult { + return this.visitFunction(n); + } + + visitFunctionExpression(n: e.FunctionExpression): VisitResult { + return this.visitFunction(n); + } + + visitArrowFunctionExpression(n: e.ArrowFunctionExpression): VisitResult { + return this.visitFunction(n); + } + + visitBlock(n: e.BlockStatement): VisitResult { + n.body = this.visitArray(n.body); + return n; + } + + visitEmptyStatement(n: e.EmptyStatement): VisitResult { + return n; + } + + visitExpressionStatement(n: e.ExpressionStatement): VisitResult { + n.expression = this.visitAs(n.expression); + return n; + } + + visitSwitch(n: e.SwitchStatement): VisitResult { + n.discriminant = this.visitAs(n.discriminant); + n.cases = this.visitArray(n.cases); + return n; + } + + visitCase(n: e.SwitchCase): VisitResult { + n.test = this.visitNullable(n.test); + n.consequent = this.visitArray(n.consequent); + return n; + } + + visitFor(n: e.ForStatement): VisitResult { + n.init = this.visitNullable(n.init); + n.test = this.visitNullable(n.test); + n.update = this.visitNullable(n.update); + n.body = this.visitAs(n.body); + return n; + } + + visitWhile(n: e.WhileStatement): VisitResult { + n.test = this.visitAs(n.test); + n.body = this.visitAs(n.body); + return n; + } + + visitIf(n: e.IfStatement): VisitResult { + n.test = this.visitAs(n.test); + n.consequent = this.visitAs(n.consequent); + n.alternate = this.visitNullable(n.alternate); + return n; + } + + visitForIn(n: e.ForInStatement): VisitResult { + n.left = this.visitAs(n.left); + n.right = this.visitAs(n.right); + n.body = this.visitAs(n.body); + return n; + } + + visitForOf(n: e.ForOfStatement): VisitResult { + n.left = this.visitAs(n.left); + n.right = this.visitAs(n.right); + n.body = this.visitAs(n.body); + return n; + } + + visitDo(n: e.DoWhileStatement): VisitResult { + n.body = this.visitAs(n.body); + n.test = this.visitAs(n.test); + return n; + } + + visitIdentifier(n: e.Identifier): VisitResult { + return n; + } + + visitLiteral(n: e.Literal): VisitResult { + return n; + } + + visitThisExpression(n: e.ThisExpression): VisitResult { + return n; + } + + visitBreak(n: e.BreakStatement): VisitResult { + return n; + } + + visitContinue(n: e.ContinueStatement): VisitResult { + return n; + } + + visitTry(n: e.TryStatement): VisitResult { + n.block = this.visitAs(n.block); + if (n.handlers) n.handlers = this.visitArray(n.handlers); + n.finalizer = this.visitNullable(n.finalizer); + return n; + } + + visitCatchClause(n: e.CatchClause): VisitResult { + n.param = this.visitAs(n.param); + n.guard = this.visitNullable(n.guard); + n.body = this.visitAs(n.body); + return n; + } + + visitThrow(n: e.ThrowStatement): VisitResult { + n.argument = this.visitAs(n.argument); + return n; + } + + visitRestElement(n: e.RestElement): VisitResult { + return n; + } + + visitReturn(n: e.ReturnStatement): VisitResult { + n.argument = this.visitNullable(n.argument); + return n; + } + + visitWith(n: e.WithStatement): VisitResult { + n.object = this.visitAs(n.object); + n.body = this.visitAs(n.body); + return n; + } + + visitYield(n: e.YieldExpression): VisitResult { + n.argument = this.visitNullable(n.argument); + return n; + } + + visitVariableDeclaration(n: e.VariableDeclaration): VisitResult { + n.declarations = this.visitArray(n.declarations); + return n; + } + + visitVariableDeclarator(n: e.VariableDeclarator): VisitResult { + n.id = this.visitAs(n.id); + if (n.init) n.init = this.visitAs(n.init); + return n; + } + + visitLabeledStatement(n: e.LabeledStatement): VisitResult { + n.label = this.visitAs(n.label); + n.body = this.visitAs(n.body); + return n; + } + + visitAssignmentExpression(n: e.AssignmentExpression): VisitResult { + n.left = this.visitAs(n.left); + n.right = this.visitAs(n.right); + return n; + } + + visitConditionalExpression(n: e.ConditionalExpression): VisitResult { + n.test = this.visitAs(n.test); + n.consequent = this.visitAs(n.consequent); + n.alternate = this.visitAs(n.alternate); + return n; + } + + visitLogicalExpression(n: e.LogicalExpression): VisitResult { + n.left = this.visitAs(n.left); + n.right = this.visitAs(n.right); + return n; + } + + visitBinaryExpression(n: e.BinaryExpression): VisitResult { + n.left = this.visitAs(n.left); + n.right = this.visitAs(n.right); + return n; + } + + visitUnaryExpression(n: e.UnaryExpression): VisitResult { + n.argument = this.visitAs(n.argument); + return n; + } + + visitUpdateExpression(n: e.UpdateExpression): VisitResult { + n.argument = this.visitAs(n.argument); + return n; + } + + visitMemberExpression(n: e.MemberExpression): VisitResult { + n.object = this.visitAs(n.object); + if (n.computed) n.property = this.visitAs(n.property); + return n; + } + + visitSequenceExpression(n: e.SequenceExpression): VisitResult { + n.expressions = this.visitArray(n.expressions); + return n; + } + + visitSuper(n: e.Super): VisitResult { + return n; + } + + visitSpreadElement(n: e.SpreadElement): VisitResult { + n.argument = this.visitAs(n.argument); + return n; + } + + visitNewExpression(n: e.NewExpression): VisitResult { + n.callee = this.visitAs(n.callee); + n.arguments = this.visitArray(n.arguments); + return n; + } + + visitObjectExpression(n: e.ObjectExpression): VisitResult { + n.properties = this.visitArray(n.properties); + return n; + } + + visitArrayExpression(n: e.ArrayExpression): VisitResult { + // esprima encodes holes in the array as 'null' elements in + // n.elements, so we can't use visitArray. instead iterate + // over the elements manually. + n.elements = this.visitArrayKeep(n.elements); + return n; + } + + visitProperty(n: e.Property): VisitResult { + n.key = this.visitAs(n.key); + n.value = this.visitAs(n.value); + return n; + } + + visitCallExpression(n: e.CallExpression): VisitResult { + n.callee = this.visitAs(n.callee); + n.arguments = this.visitArray(n.arguments); + return n; + } + + visitClassDeclaration(n: e.ClassDeclaration): VisitResult { + return this.visitClass(n); + } + + visitClassExpression(n: e.ClassExpression): VisitResult { + return this.visitClass(n); + } + + visitClass(n: e.Class): VisitResult { + n.body = this.visitAs(n.body); + return n; + } + + visitClassBody(n: e.ClassBody): VisitResult { + n.body = this.visitArray(n.body); + return n; + } + + visitMetaProperty(n: e.MetaProperty): VisitResult { + return n; + } + + visitMethodDefinition(n: e.MethodDefinition): VisitResult { + n.value = this.visitAs(n.value); + return n; + } + + visitExportDefaultDeclaration(n: e.ExportDefaultDeclaration): VisitResult { + n.declaration = this.visitAs(n.declaration); + return n; + } + + visitExportNamedDeclaration(n: e.ExportNamedDeclaration): VisitResult { + n.declaration = this.visitNullable(n.declaration); + // XXX specifiers? + return n; + } + + visitExportAllDeclaration(n: e.ExportAllDeclaration): VisitResult { + return n; + } + + visitExportSpecifier(n: e.ExportSpecifier): VisitResult { + return n; + } + + visitImportDeclaration(n: e.ImportDeclaration): VisitResult { + n.specifiers = this.visitArray(n.specifiers); + return n; + } + + visitImportSpecifier(n: e.ImportSpecifier): VisitResult { + n.imported = this.visitAs(n.imported); + return n; + } + + visitImportDefaultSpecifier(n: e.ImportDefaultSpecifier): VisitResult { + return n; + } + + visitImportNamespaceSpecifier(n: e.ImportNamespaceSpecifier): VisitResult { + return n; + } + + visitArrayPattern(n: e.ArrayPattern): VisitResult { + n.elements = this.visitArrayKeep(n.elements); + return n; + } + + visitAssignmentPattern(n: e.AssignmentPattern): VisitResult { + // the left side is a binding pattern, not a reference + n.right = this.visitAs(n.right); + return n; + } + + visitObjectPattern(n: e.ObjectPattern): VisitResult { + n.properties = this.visitArray(n.properties); + return n; + } + + visitTaggedTemplateExpression(n: e.TaggedTemplateExpression): VisitResult { + n.quasi = this.visitAs(n.quasi); + return n; + } + + visitTemplateLiteral(n: e.TemplateLiteral): VisitResult { + n.quasis = this.visitArray(n.quasis); + n.expressions = this.visitArray(n.expressions); + return n; + } + + visitTemplateElement(n: e.TemplateElement): VisitResult { + return n; + } + + toString(): string { + return "TreeVisitor"; + } +} + +export class TransformPass extends TreeVisitor { + options: CompilerOptions; + filename: string; + + constructor(options: CompilerOptions, filename?: string) { + super(); + this.options = options; + this.filename = filename ?? ""; + } +} diff --git a/lib/optimizations.js b/lib/optimizations.js deleted file mode 100644 index 1856a31d..00000000 --- a/lib/optimizations.js +++ /dev/null @@ -1,20 +0,0 @@ -/* -*- Mode: js2; indent-tabs-mode: nil; tab-width: 4; js2-indent-offset: 4; js2-basic-offset: 4; -*- - * vim: set ts=4 sw=4 et tw=99 ft=js: - */ -import * as escodegen from "../external-deps/escodegen/escodegen-es6"; - -import * as debug from "./debug"; - -import { ReplaceUnaryVoid } from "./passes/replace-unary-void"; - -const passes = [ReplaceUnaryVoid]; - -export function run(tree) { - passes.forEach((passType) => { - let pass = new passType(); - tree = pass.visit(tree); - debug.log(2, `after: ${passType.name}`); - debug.log(2, () => escodegen.generate(tree)); - }); - return tree; -} diff --git a/lib/options.ts b/lib/options.ts new file mode 100644 index 00000000..63ab809a --- /dev/null +++ b/lib/options.ts @@ -0,0 +1,43 @@ +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: + */ + +// The driver's options object (defaults live in ejs-es6.ts). The passes +// and the compiler consume slices of this; it is threaded everywhere. + +export interface ImportVariable { + variable: string; + value: string; +} + +export interface OutputWriter { + write(msg: string, want_newline?: boolean): void; +} + +export interface CompilerOptions { + opt_level: number; + debug: boolean; + debug_level: number; + debug_passes: Set; + warn_on_undeclared: boolean; + frozen_global: boolean; + record_types: boolean; + // MAAM type-analysis probe: run the analysis and + // log stats; codegen consumes nothing yet. Distinct from record_types + // (the runtime type-recording instrumentation). + types: boolean; + // --types plus a per-binding type dump (implies types). + types_dump: boolean; + output_filename: string | null; + show_help: boolean; + leave_temp_files: boolean; + native_module_dirs: string[]; + extra_clang_args: string; + ios_sdk: string; + ios_min: string; + osx_min: string; + import_variables: ImportVariable[]; + srcdir: boolean; + stdout_writer: OutputWriter; + quiet?: boolean; +} diff --git a/lib/pass-config.ts b/lib/pass-config.ts new file mode 100644 index 00000000..51191fc4 --- /dev/null +++ b/lib/pass-config.ts @@ -0,0 +1,344 @@ +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: + */ + +// Pass configuration (compiler-P5): the clang-style -O/-f surface. +// +// One registry table maps each canonical pass name to its PassConfig +// field, its default at each -O level, and its help text; the driver's +// --help and --print-passes listings are generated from it so they +// can't drift. Passes read the resolved snapshot via passes() — never +// process.env, which under the self-hosted runtime is a +// rebuild-the-whole-environment getter (the SinkFlags lesson in +// optimize.ts, now the rule for every pass). +// +// Resolution order (gcc semantics): the -O suite's defaults, then +// -f/-fno- overrides in command-line order, last-wins. +// EJS_FLAGS in the environment is tokenized and applied after the real +// argv by the driver — the single debugging escape for harnesses that +// don't thread driver flags. +// +// Suites: -O0 is straight lowering (no EIR optimizer; the lowering and +// emission behaviors that were never opt_level-gated stay on — see the +// per-pass levels below); -O1 is the cheap always-sound intra-function +// tier; -O2 (the default) adds the module-level tier and is exactly the +// pre-P5 default pipeline; -O3 is -O2 on the EIR side (the LLVM +// pipeline still runs default — -fllvm-opt= decouples it). + +export interface PassConfig { + // the EIR optimizer (integrate.ts drives, optimize.ts runs) + eirOpt: boolean; + eirCleanup: boolean; + slotCse: boolean; + shapedSink: boolean; + argsSink: boolean; + flowSink: boolean; + // the module-level tier + devirt: boolean; + eirSpec: boolean; + exportWrapper: boolean; + ctorSink: boolean; + shapeFusion: boolean; + // lowering-time behaviors (oracle-gated where applicable) + shapeGuards: boolean; + polyShapeGuards: boolean; + bornShaped: boolean; + promote: boolean; + // -fno-promote=: decline promotion only for module paths + // containing one of the substrings (the old EJS_NO_PROMOTE list) + promoteExclude: string[]; + // emission controls (emit.ts) + gcFrames: boolean; + inlineAlloc: boolean; + inlineEnvSlots: boolean; + // opt-in probes + lowtier: boolean; + // LLVM pipeline level escape hatch: null = follow the -O level + llvmOpt: number | null; +} + +// which boolean field a pass name controls (promoteExclude and llvmOpt +// are the two valued knobs, handled specially in applyFlag) +type BoolField = { + [K in keyof PassConfig]: PassConfig[K] extends boolean ? K : never; +}[keyof PassConfig]; + +export interface PassDesc { + name: string; // canonical -f/-fno- spelling + field: BoolField; + // lowest -O level the pass defaults on at (0 = always, including + // -O0; OPT_IN = never — only an explicit -f enables it) + minLevel: number; + help: string; +} + +const OPT_IN = 99; + +export const PASSES: readonly PassDesc[] = [ + { + name: "eir-opt", + field: "eirOpt", + minLevel: 1, + help: "the EIR optimizer as a whole; off = straight lowering to LLVM", + }, + { + name: "eir-cleanup", + field: "eirCleanup", + minLevel: 1, + help: "constant folding, trivial-param pruning, typeof/boolean rewrites, lattice-typed f64 lowering", + }, + { + name: "slot-cse", + field: "slotCse", + minLevel: 1, + help: "module-slot load CSE over stable %self slots", + }, + { + name: "shaped-sink", + field: "shapedSink", + minLevel: 1, + help: "scalar replacement of non-escaping shaped literals", + }, + { + name: "args-sink", + field: "argsSink", + minLevel: 1, + help: "rest/arguments objects used only for .length fold to arg_len", + }, + { + name: "flow-sink", + field: "flowSink", + minLevel: 1, + help: "flow-sensitive sinking of written/partially-escaping literals", + }, + { + name: "devirt", + field: "devirt", + minLevel: 2, + help: "direct-call devirtualization of module-local closures", + }, + { + name: "eir-spec", + field: "eirSpec", + minLevel: 2, + help: "oracle-driven function specialization (needs --types)", + }, + { + name: "export-wrapper", + field: "exportWrapper", + minLevel: 2, + help: "guarded entry wrappers so escaping functions keep specialized clones (needs --types)", + }, + { + name: "ctor-sink", + field: "ctorSink", + minLevel: 2, + help: "epoch-guarded constructor-result sinking (needs --types)", + }, + { + name: "shape-fusion", + field: "shapeFusion", + minLevel: 2, + help: "heterogeneous shape+numeric region merging and in-loop numeric folding", + }, + { + name: "shape-guards", + field: "shapeGuards", + minLevel: 0, + help: "has_shape guard diamonds on oracle-known receivers (needs --types)", + }, + { + name: "poly-shape-guards", + field: "polyShapeGuards", + minLevel: 0, + help: "2-way polymorphic shape-guard chains (needs --types)", + }, + { + name: "born-shaped", + field: "bornShaped", + minLevel: 0, + help: "object literals and constructor prefixes allocate at their birth shape", + }, + { + name: "promote", + field: "promote", + minLevel: 0, + help: "promote non-exported module-level vars to hidden module slots; -fno-promote= declines only matching module paths", + }, + { + name: "gc-frames", + field: "gcFrames", + minLevel: 0, + help: "precise GC frames for values live across safepoints", + }, + { + name: "inline-alloc", + field: "inlineAlloc", + minLevel: 0, + help: "inline bump allocation for environments", + }, + { + name: "inline-env-slots", + field: "inlineEnvSlots", + minLevel: 0, + help: "inline env slot addressing instead of runtime accessor calls", + }, + { + name: "lowtier", + field: "lowtier", + minLevel: OPT_IN, + help: "swap the lowtier_* probe function bodies for hand-built low-tier EIR (test hook)", + }, +]; + +const byName = new Map(PASSES.map((p) => [p.name, p])); + +export function defaultPassConfig(optLevel: number): PassConfig { + const cfg = { + promoteExclude: [], + llvmOpt: null, + } as unknown as PassConfig; + for (const p of PASSES) cfg[p.field] = optLevel >= p.minLevel; + return cfg; +} + +// apply one -f/-fno- token. returns an error message, or null on +// success. `prov` (when given) records the token as each touched +// setting's provenance, for --print-passes. +export function applyPassFlag( + cfg: PassConfig, + token: string, + prov?: Map +): string | null { + if (token.indexOf("-f") !== 0) return `not a pass flag: ${token}`; + let body = token.substring(2); + let enable = true; + if (body.indexOf("no-") === 0) { + enable = false; + body = body.substring(3); + } + let value: string | null = null; + const eq = body.indexOf("="); + if (eq !== -1) { + value = body.substring(eq + 1); + body = body.substring(0, eq); + } + + // the LLVM-side escape hatch is a valued knob, not a registry pass + if (body === "llvm-opt") { + if (!enable) { + if (value !== null) return `-fno-llvm-opt does not take a value`; + cfg.llvmOpt = 0; + } else { + const n = value === null ? NaN : parseInt(value, 10); + if (!(n >= 0 && n <= 3)) return `-fllvm-opt wants =<0..3>, got '${token}'`; + cfg.llvmOpt = n; + } + if (prov) prov.set("llvm-opt", token); + return null; + } + + const desc = byName.get(body); + if (!desc) { + return `unknown pass '${body}' in ${token} (see --print-passes for the list)`; + } + if (value !== null) { + // -fno-promote= is the one valued spelling: decline + // promotion only for matching module paths + if (desc.name !== "promote" || enable) + return `pass '${desc.name}' does not take a value: ${token}`; + cfg.promote = true; + cfg.promoteExclude = value.split(",").filter((s) => s.length > 0); + } else { + cfg[desc.field] = enable; + if (desc.name === "promote") cfg.promoteExclude = []; + } + if (prov) prov.set(desc.name, token); + return null; +} + +export interface ResolvedPasses { + config: PassConfig; + // canonical name -> what decided it ("-O2 suite" or the flag token) + provenance: Map; + errors: string[]; +} + +export function resolvePassConfig(optLevel: number, flagTokens: string[]): ResolvedPasses { + const config = defaultPassConfig(optLevel); + const provenance = new Map(); + for (const p of PASSES) provenance.set(p.name, `-O${optLevel} suite`); + provenance.set("llvm-opt", `-O${optLevel} suite`); + const errors: string[] = []; + for (const token of flagTokens) { + const err = applyPassFlag(config, token, provenance); + if (err) errors.push(err); + } + return { config, provenance, errors }; +} + +// --- the per-run snapshot --------------------------------------------------- + +// the driver resolves once at startup and installs; library callers and +// the unit tests get today's default pipeline (-O2) unless they say +// otherwise. Snapshot semantics: mutate only through set/with below. +let current: PassConfig = defaultPassConfig(2); + +export function passes(): PassConfig { + return current; +} + +export function setPassConfig(cfg: PassConfig): void { + current = cfg; +} + +// tests: run f with named settings overridden, restoring on the way out +export function withPassConfig(overrides: Partial, f: () => T): T { + const prev = current; + current = { ...prev, ...overrides }; + try { + return f(); + } finally { + current = prev; + } +} + +// --- generated listings ----------------------------------------------------- + +// the --help section: one line per pass, from the registry +export function formatPassHelp(): string { + const lines: string[] = []; + lines.push("Pass flags (-f enables, -fno- disables; applied after the -O suite,"); + lines.push("last one wins). Defaults: [0] on at every level incl. -O0, [1] on at -O1+,"); + lines.push("[2] on at -O2+, [-] off unless enabled explicitly:"); + for (const p of PASSES) { + const lvl = p.minLevel === OPT_IN ? "-" : String(p.minLevel); + lines.push(` -f[no-]${p.name} [${lvl}] ${p.help}`); + } + lines.push( + " -fllvm-opt=<0..3> [=] run the LLVM pipeline at this level instead of the -O level" + ); + return lines.join("\n"); +} + +// the --print-passes listing: the effective configuration and where +// each setting came from +export function formatEffectiveConfig(r: ResolvedPasses, optLevel: number): string { + const lines: string[] = []; + lines.push(`effective pass configuration at -O${optLevel}:`); + for (const p of PASSES) { + const on = r.config[p.field]; + let state = on ? "on " : "off"; + if (p.name === "promote" && on && r.config.promoteExclude.length > 0) + state = `on (except ${r.config.promoteExclude.join(",")})`; + lines.push( + ` ${p.name.padEnd(18)} ${state.padEnd(6)} (${r.provenance.get(p.name) || "?"})` + ); + } + const llvm = r.config.llvmOpt === null ? `O${optLevel}` : `O${r.config.llvmOpt}`; + lines.push( + ` ${"llvm-opt".padEnd(18)} ${llvm.padEnd(6)} (${r.provenance.get("llvm-opt") || "?"})` + ); + return lines.join("\n"); +} + diff --git a/lib/passes/desugar-arguments.js b/lib/passes/desugar-arguments.js deleted file mode 100644 index f9f82106..00000000 --- a/lib/passes/desugar-arguments.js +++ /dev/null @@ -1,40 +0,0 @@ -import { reportError } from "../errors"; -import { argPresent_id, getArg_id, getArgumentsObject_id } from "../common-ids"; -import { intrinsic } from "../echo-util"; -import { TransformPass } from "../node-visitor"; -import * as b from "../ast-builder"; - -export class DesugarArguments extends TransformPass { - constructor(options, filename) { - super(options); - this.filename = filename; - } - - visitIdentifier(n) { - if (n.name === "arguments") return intrinsic(getArgumentsObject_id); - return super.visitIdentifier(n); - } - - visitVariableDeclarator(n) { - if (n.id.name === "arguments") - reportError( - SyntaxError, - "Cannot declare variable named 'arguments'", - this.filename, - n.id.loc - ); - return super.visitVariableDeclarator(n); - } - - visitAssignmentExpression(n) { - if (n.left.type === b.Identifier && n.left.name === "arguments") - reportError(SyntaxError, "Cannot set 'arguments'", this.filename, n.left.loc); - return super.visitAssignmentExpression(n); - } - - visitProperty(n) { - if (n.computed) n.key = this.visit(n.key); - n.value = this.visit(n.value); - return n; - } -} diff --git a/lib/passes/desugar-arrow-functions.js b/lib/passes/desugar-arrow-functions.js deleted file mode 100644 index 1a244605..00000000 --- a/lib/passes/desugar-arrow-functions.js +++ /dev/null @@ -1,122 +0,0 @@ -/* -*- Mode: js2; indent-tabs-mode: nil; tab-width: 4; js2-indent-offset: 4; js2-basic-offset: 4; -*- - * vim: set ts=4 sw=4 et tw=99 ft=js: - */ -// this pass converts all arrow functions to normal anonymous function -// expressions with a closed-over 'this' -// -// take the following: -// -// function foo() { -// let mapper = (arr) => { -// arr.map (el => el * this.x); -// }; -// } -// -// This will be compiled to: -// -// function foo() { -// let _this_010 = this; -// let mapper = function (arr) { -// arr.map (function (el) { return el * _this_010.x; }); -// }; -// } -// -// and the usual closure conversion stuff will make sure the bindings -// exists in the closure env as usual. -// - -import { TransformPass } from "../node-visitor"; -import * as b from "../ast-builder"; -import { startGenerator } from "../echo-util"; -import { reportError } from "../errors"; - -function definesThis(n) { - return n.type === b.FunctionDeclaration || n.type === b.FunctionExpression; -} - -export class DesugarArrowFunctions extends TransformPass { - constructor(options) { - super(options); - this.mapping = []; - this.thisGen = startGenerator(); - } - - visitArrowFunctionExpression(n) { - if (n.expression) { - n.body = b.blockStatement([b.returnStatement(n.body)], n.body.loc); - n.expression = false; - } - n = this.visitFunction(n); - n.type = b.FunctionExpression; - return n; - } - - visitThisExpression(n) { - if (this.mapping.length === 0) { - // a 'this' at toplevel. not possible in ejs, since we wrap everything in toplevel functions - return b.undefinedLit(); - } - - let topfunc = this.mapping[0].func; - - for (let m of this.mapping) { - if (definesThis(m.func)) { - // if we're already on top, just return the existing thisExpression - if (topfunc === m.func) return n; - - if (m.this_id) return b.identifier(m.this_id); - - m.this_id = `_this_${this.thisGen()}`; - - m.prepend = b.letDeclaration(b.identifier(m.this_id), b.thisExpression()); - - return b.identifier(m.this_id); - } - } - - reportError( - SyntaxError, - 'no binding for "this" available for arrow function', - this.filename, - n.loc - ); - } - - visitIdentifier(n) { - if (n.name !== "arguments") return super.visitIdentifier(n); - - if (this.mapping.length > 0) { - let topfunc = this.mapping[0].func; - - for (let m of this.mapping) { - if (definesThis(m.func)) { - // if we're already on top, just return the existing thisExpression - if (topfunc === m.func) return n; - - if (m.arguments_id) return b.identifier(m.arguments_id); - - m.arguments_id = `_arguments_${this.thisGen()}`; - - m.prepend = b.letDeclaration(b.identifier(m.arguments_id), n); - - return b.identifier(m.arguments_id); - } - } - - reportError( - SyntaxError, - 'no binding for "arguments" available for arrow function', - this.filename, - n.loc - ); - } - } - - visitFunction(n) { - this.mapping.unshift({ func: n, id: null }); - n = super.visitFunction(n); - let m = this.mapping.shift(); - if (m.prepend) n.body.body.unshift(m.prepend); - return n; - } -} diff --git a/lib/passes/desugar-classes.js b/lib/passes/desugar-classes.js deleted file mode 100644 index 4be658db..00000000 --- a/lib/passes/desugar-classes.js +++ /dev/null @@ -1,433 +0,0 @@ -/* -*- Mode: js2; indent-tabs-mode: nil; tab-width: 4; js2-indent-offset: 4; js2-basic-offset: 4; -*- - * vim: set ts=4 sw=4 et tw=99 ft=js: - */ -// -// converts: -// -// class Subclass extends Baseclass -// constructor (/* ctor args */) { /* ctor body */ } -// -// method (/* method args */) { /* method body */ } -// -// to: -// -// let Subclass = (function(%super) { -// function Subclass (/* ctor args */) { /* ctor body */ } -// Subclass.prototype.method = function(/* method args */) { /* method body */ }; -// return Subclass; -// })(Baseclass) -// -// -import * as b from "../ast-builder"; -import { - setConstructorKindDerived_id, - setConstructorKindBase_id, - superid, - constructSuper_id, - constructSuperApply_id, - setPrototypeOf_id, - objectCreate_id, - call_id, - prototype_id, - constructor_id, - get_id, - set_id, - Object_id, - proto_id, - defineProperty_id, - enumerable_id, - value_id, - defineProperties_id, -} from "../common-ids"; -import { Stack } from "../stack-es6"; -import { reportError } from "../errors"; -import { TransformPass } from "../node-visitor"; -import { intrinsic, startGenerator } from "../echo-util"; - -import * as escodegen from "../../external-deps/escodegen/escodegen-es6"; - -function createSuperReference(is_static, id) { - if (id && id.name === "constructor") return superid; - - let obj = is_static ? superid : b.memberExpression(superid, prototype_id); - - if (!id) return obj; - - return b.memberExpression(obj, id); -} - -let classgen = startGenerator(); -function freshClassId() { - return b.identifier(`%anonClass_${classgen()}`); -} - -export class DesugarClasses extends TransformPass { - constructor(options) { - super(options); - this.class_stack = new Stack(); - this.method_stack = new Stack(); - } - - visitCallExpression(n) { - if (n.callee.type === b.Super) { - if (this.method_stack.top.key.name !== "constructor") { - reportError( - SyntaxError, - "calls to super() are only allowable in constructors.", - this.filename, - n.callee.loc - ); - } - - let super_ref = createSuperReference( - this.method_stack.top.static, - this.method_stack.top.key - ); - n.callee = constructSuper_id; - n.arguments.unshift(super_ref); - } else if (n.callee.type === b.MemberExpression && n.callee.object.type === b.Super) { - let super_ref = createSuperReference( - this.method_stack.top.static, - this.method_stack.top.key - ); - n.callee = b.memberExpression(super_ref, call_id); - n.arguments.unshift(b.thisExpression()); - } else { - n.callee = this.visit(n.callee); - } - n.arguments = this.visitArray(n.arguments); - return n; - } - - visitNewExpression(n) { - n.callee = this.visit(n.callee); - n.arguments = this.visitArray(n.arguments); - return n; - } - - visitObjectExpression(n) { - for (let property of n.properties) { - if (property.computed) property.key = this.visit(property.key); - property.value = this.visit(property.value); - } - return n; - } - - visitSuper() { - return createSuperReference(this.method_stack.top.static); - } - - visitClassDeclaration(n) { - if (!n.id) n.id = freshClassId(); - n.superClass = this.visit(n.superClass); - let iife = this.generateClassIIFE(n); - return b.letDeclaration(n.id, b.callExpression(iife, n.superClass ? [n.superClass] : [])); - } - - visitClassExpression(n) { - if (!n.id) n.id = freshClassId(); - n.superClass = this.visit(n.superClass); - let iife = this.generateClassIIFE(n); - return b.callExpression(iife, n.superClass ? [n.superClass] : []); - } - - generateClassIIFE(n) { - // we visit all the functions defined in the class so that 'super' is replaced with '%super' - this.class_stack.push(n); - - // XXX this push/pop should really be handled in this.visitMethodDefinition - for (let class_element of n.body.body) { - this.method_stack.push(class_element); - class_element.value = this.visit(class_element.value); - this.method_stack.pop(); - } - - this.class_stack.pop(); - - let class_init_iife_body = []; - - let [properties, methods, sproperties, smethods] = this.gather_members(n); - - class_init_iife_body.push( - b.letDeclaration(b.identifier("proto"), b.memberExpression(n.id, prototype_id)) - ); - - let ctor = null; - methods.forEach((m, mkey) => { - // if it's a method with name 'constructor' output the special ctor function - if (mkey === "constructor") { - ctor = m; - } else { - class_init_iife_body.push(this.create_proto_method(m, n)); - } - }); - smethods.forEach((sm) => class_init_iife_body.push(this.create_static_method(sm, n))); - - let proto_props = this.create_properties(properties, n, false); - if (proto_props) class_init_iife_body = class_init_iife_body.concat(proto_props); - - let static_props = this.create_properties(sproperties, n, true); - if (static_props) class_init_iife_body = class_init_iife_body.concat(static_props); - - // generate and prepend a default ctor if there isn't one declared. - // It looks like this in code: - // function Subclass (...args) { %super.call(this, args...); } - if (!ctor) { - ctor = this.create_default_constructor(n); - - // we didn't visit it above, so do it now - this.method_stack.push(ctor); - ctor.value = this.visit(ctor.value); - this.method_stack.pop(); - } - - let ctor_func = this.create_constructor(ctor, n); - if (n.superClass) { - class_init_iife_body.unshift( - b.expressionStatement( - b.assignmentExpression( - b.memberExpression(b.memberExpression(n.id, prototype_id), constructor_id), - "=", - n.id - ) - ) - ); - - // also set ctor.prototype = Object.create(superClass.prototype) - class_init_iife_body.unshift( - b.expressionStatement( - b.callExpression(setPrototypeOf_id, [ - b.memberExpression(ctor_func.id, prototype_id), - b.callExpression(objectCreate_id, [ - b.memberExpression(superid, prototype_id), - ]), - ]) - ) - ); - - // 14.5.17 step 9, make sure the constructor's __proto__ is set to superClass - class_init_iife_body.unshift( - b.expressionStatement(b.callExpression(setPrototypeOf_id, [ctor_func.id, superid])) - ); - - class_init_iife_body.unshift( - b.expressionStatement(intrinsic(setConstructorKindDerived_id, [ctor_func.id])) - ); - } else { - class_init_iife_body.unshift( - b.expressionStatement(intrinsic(setConstructorKindBase_id, [ctor_func.id])) - ); - } - - class_init_iife_body.unshift(ctor_func); - - // make sure we return the function from our iife - class_init_iife_body.push(b.returnStatement(n.id)); - - // (function (%super?) { ... }) - let iife_body = b.blockStatement(class_init_iife_body, n.loc); - return b.functionExpression( - b.identifier(`${n.id.name || "anonclass"}_iife`), - n.superClass ? [superid] : [], - iife_body, - [], - null, - n.loc - ); - } - - gather_members(ast_class) { - let methods = new Map(); - let smethods = new Map(); - let properties = new Map(); - let sproperties = new Map(); - - for (let class_element of ast_class.body.body) { - let class_element_name = this.nameOfKey(class_element.key); - if (class_element.static && class_element_name === "prototype") - reportError( - SyntaxError, - 'Illegal method name "prototype" on static class member.', - this.filename, - class_element.loc - ); - - if (class_element.kind === "method" || class_element.kind === "constructor") { - // a method - let method_map = class_element.static ? smethods : methods; - if (method_map.has(class_element_name)) - reportError( - SyntaxError, - `method '${class_element_name}' has already been defined.`, - this.filename, - class_element.loc - ); - method_map.set(class_element_name, class_element); - } else { - // a property - let property_map = class_element.static ? sproperties : properties; - - if (!property_map.has(class_element.key)) - property_map.set(class_element.key, new Map()); - - if (property_map.get(class_element.key).has(class_element.kind)) - reportError( - SyntaxError, - `a '${class_element.kind}' method for '${escodegen.generate( - class_element.key - )}' has already been defined.`, - this.filename, - class_element.loc - ); - - if (class_element.kind === "set") { - if (class_element.value.params.length > 0) { - let last_param = - class_element.value.params[class_element.value.params.length - 1]; - if (last_param.type == b.RestElement) - reportError( - SyntaxError, - "Setters are not allowed to have a rest", - this.filename, - last_param.loc - ); - } - } - - // XXX this doesn't work for properties where one accessor is computed and the other isn't... - let computed = class_element.computed; - - if (property_map.get(class_element.key).has("computed")) { - if (computed != property_map.get(class_element.key).get("computed")) - reportError( - Error, - "unsupported mismatch computed state for property accessors", - this.filename, - class_element.loc - ); - } - - property_map.get(class_element.key).set(class_element.kind, class_element); - - property_map.get(class_element.key).set("computed", computed); - } - } - - return [properties, methods, sproperties, smethods]; - } - - create_constructor(ast_method, ast_class) { - return b.functionDeclaration( - ast_class.id, - ast_method.value.params, - ast_method.value.body, - ast_method.value.defaults, - ast_method.value.rest - ); - } - - create_default_constructor(ast_class) { - // splat args into the call to super's ctor if there's a superclass - let args_id = b.identifier("args"); - let functionBody = b.blockStatement( - ast_class.superClass - ? [b.expressionStatement(intrinsic(constructSuperApply_id, [superid, args_id]))] - : [] - ); - return b.methodDefinition( - constructor_id, - b.functionExpression(null, [b.restElement(args_id)], functionBody, []) - ); - } - - nameOfKey(key) { - return key.type == b.Identifier ? key.name : key.value; - } - - create_proto_method(ast_method, ast_class) { - let method_name = this.nameOfKey(ast_method.key); - let method_key = ast_method.computed ? ast_method.key : b.literal(method_name); - let method = b.functionExpression( - b.identifier(`${ast_class.id.name}:${method_name}`), - ast_method.value.params, - ast_method.value.body, - ast_method.value.defaults, - ast_method.value.rest - ); - - let Object_defineProperty = b.memberExpression(Object_id, defineProperty_id); - let defineProperty_args = b.objectExpression([ - b.property(value_id, method), - b.property(enumerable_id, b.literal(false)), - ]); - return b.expressionStatement( - b.callExpression(Object_defineProperty, [proto_id, method_key, defineProperty_args]) - ); - } - - create_static_method(ast_method, ast_class) { - let method_name = this.nameOfKey(ast_method.key); - let method_key = ast_method.computed ? ast_method.key : b.literal(method_name); - let method = b.functionExpression( - ast_method.key, - ast_method.value.params, - ast_method.value.body, - ast_method.value.defaults, - ast_method.value.rest - ); - - let Object_defineProperty = b.memberExpression(Object_id, defineProperty_id); - let defineProperty_args = b.objectExpression([ - b.property(value_id, method), - b.property(enumerable_id, b.literal(false)), - ]); - return b.expressionStatement( - b.callExpression(Object_defineProperty, [ast_class.id, method_key, defineProperty_args]) - ); - } - - create_properties(properties, ast_class, are_static) { - let propdescs = []; - - properties.forEach((prop_map, prop) => { - let accessors = []; - let key = null; - - let getter = prop_map.get("get"); - let setter = prop_map.get("set"); - - if (getter) { - accessors.push(b.property(get_id, getter.value)); - key = prop; - } - if (setter) { - accessors.push(b.property(set_id, setter.value)); - key = prop; - } - - propdescs.push( - b.property( - key, - b.objectExpression(accessors), - "init", - prop_map.get("computed") == true - ) - ); - }); - - if (propdescs.length === 0) return null; - - let propdescs_literal = b.objectExpression(propdescs); - - let target; - if (are_static) target = ast_class.id; - else target = b.identifier("proto"); - - return b.expressionStatement( - b.callExpression(b.memberExpression(Object_id, defineProperties_id), [ - target, - propdescs_literal, - ]) - ); - } -} diff --git a/lib/passes/desugar-classes.ts b/lib/passes/desugar-classes.ts new file mode 100644 index 00000000..0ad8a1d7 --- /dev/null +++ b/lib/passes/desugar-classes.ts @@ -0,0 +1,483 @@ +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: + */ +// +// converts: +// +// class Subclass extends Baseclass +// constructor (/* ctor args */) { /* ctor body */ } +// +// method (/* method args */) { /* method body */ } +// +// to: +// +// let Subclass = (function(%super) { +// function Subclass (/* ctor args */) { /* ctor body */ } +// Subclass.prototype.method = function(/* method args */) { /* method body */ }; +// return Subclass; +// })(Baseclass) +// +// +import * as b from "../ast-builder"; +import { + setConstructorKindDerived_id, + setConstructorKindBase_id, + superid, + constructSuper_id, + constructSuperApply_id, + setPrototypeOf_id, + objectCreate_id, + call_id, + prototype_id, + constructor_id, + get_id, + set_id, + Object_id, + proto_id, + defineProperty_id, + enumerable_id, + value_id, + defineProperties_id, +} from "../common-ids"; +import { Stack } from "../stack-es6"; +import { reportError } from "../errors"; +import { TransformPass, VisitResult } from "../node-visitor"; +import { intrinsic, startGenerator } from "../echo-util"; +import type * as e from "../estree"; + +// identifiers that appear in VALUE position must be fresh AST nodes per +// use: the EIR scope analysis resolves references in a map keyed by node, +// so a shared singleton node (like common-ids' superid) walked in two +// class iifes would resolve every occurrence to the LAST iife's binding. +// property-position identifiers (`.prototype`, object keys) are never +// resolved and may stay shared. +function freshSuper(): e.Identifier { + return b.identifier(superid.name); +} +function freshProto(): e.Identifier { + return b.identifier(proto_id.name); +} + +function createSuperReference(is_static: boolean, id?: e.Expression): e.Expression { + if (id && id.type === "Identifier" && id.name === "constructor") return freshSuper(); + + const obj = is_static ? freshSuper() : b.memberExpression(freshSuper(), prototype_id); + + if (!id) return obj; + + return b.memberExpression(obj, id); +} + +// a class node whose (possibly synthesized) id is known present +type NamedClass = e.ClassBase & { id: e.Identifier }; + +const classgen = startGenerator(); +function freshClassId(): e.Identifier { + return b.identifier(`%anonClass_${classgen()}`); +} + +// one prototype/static property's accessors (a get/set pair for the same +// non-computed name shares an entry — keying by the key AST node lost the +// getter, latent bug #14/#26) +interface AccessorEntry { + get?: e.MethodDefinition; + set?: e.MethodDefinition; + computed: boolean; +} + +export class DesugarClasses extends TransformPass { + private method_stack = new Stack(); + + override visitCallExpression(n: e.CallExpression): VisitResult { + if (n.callee.type === "Super") { + const method = this.method_stack.top; + if (this.nameOfKey(method.key) !== "constructor") { + reportError( + SyntaxError, + "calls to super() are only allowable in constructors.", + this.filename, + n.callee.loc ?? undefined + ); + } + + const super_ref = createSuperReference(method.static === true, method.key); + n.callee = constructSuper_id; + n.arguments.unshift(super_ref); + } else if (n.callee.type === "MemberExpression" && n.callee.object.type === "Super") { + const method = this.method_stack.top; + const super_ref = createSuperReference(method.static === true, method.key); + n.callee = b.memberExpression(super_ref, call_id); + n.arguments.unshift(b.thisExpression()); + } else { + n.callee = this.visitAs(n.callee); + } + n.arguments = this.visitArray(n.arguments); + return n; + } + + override visitNewExpression(n: e.NewExpression): VisitResult { + n.callee = this.visitAs(n.callee); + n.arguments = this.visitArray(n.arguments); + return n; + } + + override visitObjectExpression(n: e.ObjectExpression): VisitResult { + for (const property of n.properties) { + if (property.computed) property.key = this.visitAs(property.key); + property.value = this.visitAs(property.value); + } + return n; + } + + override visitSuper(): VisitResult { + return createSuperReference(this.method_stack.top.static === true); + } + + override visitClassDeclaration(n: e.ClassDeclaration): VisitResult { + if (!n.id) n.id = freshClassId(); + n.superClass = this.visitNullable(n.superClass); + const iife = this.generateClassIIFE(n); + return b.letDeclaration(n.id, b.callExpression(iife, n.superClass ? [n.superClass] : [])); + } + + override visitClassExpression(n: e.ClassExpression): VisitResult { + if (!n.id) n.id = freshClassId(); + n.superClass = this.visitNullable(n.superClass); + const iife = this.generateClassIIFE(n as NamedClass); + return b.callExpression(iife, n.superClass ? [n.superClass] : []); + } + + private generateClassIIFE(n: NamedClass): e.FunctionExpression { + // visit all the functions defined in the class so that 'super' is + // replaced with '%super' + for (const class_element of n.body.body) { + this.method_stack.push(class_element); + class_element.value = this.visitAs(class_element.value); + this.method_stack.pop(); + } + + let class_init_iife_body: e.Statement[] = []; + + const { properties, methods, sproperties, smethods } = this.gather_members(n); + + // a fresh node per value-position use of the class name: n.id + // itself becomes the OUTER let declarator (visitClassDeclaration), + // and node-keyed reference resolution must not alias the two scopes + const cname = () => b.identifier(n.id.name); + + class_init_iife_body.push( + b.letDeclaration(b.identifier("proto"), b.memberExpression(cname(), prototype_id)) + ); + + let ctor: e.MethodDefinition | null = null; + methods.forEach((m, mkey) => { + // the method named 'constructor' becomes the special ctor function + if (mkey === "constructor") { + ctor = m; + } else { + class_init_iife_body.push(this.create_proto_method(m, n)); + } + }); + smethods.forEach((sm) => class_init_iife_body.push(this.create_static_method(sm, n))); + + const proto_props = this.create_properties(properties, n, false); + if (proto_props) class_init_iife_body.push(proto_props); + + const static_props = this.create_properties(sproperties, n, true); + if (static_props) class_init_iife_body.push(static_props); + + // generate and prepend a default ctor if there isn't one declared. + // It looks like this in code: + // function Subclass (...args) { %super.call(this, args...); } + if (!ctor) { + ctor = this.create_default_constructor(n); + + // we didn't visit it above, so do it now + this.method_stack.push(ctor); + ctor.value = this.visitAs(ctor.value); + this.method_stack.pop(); + } + + const ctor_func = this.create_constructor(ctor, n); + if (n.superClass) { + class_init_iife_body.unshift( + b.expressionStatement( + b.assignmentExpression( + b.memberExpression( + b.memberExpression(cname(), prototype_id), + constructor_id + ), + "=", + cname() + ) + ) + ); + + // also set ctor.prototype = Object.create(superClass.prototype) + class_init_iife_body.unshift( + b.expressionStatement( + b.callExpression(setPrototypeOf_id, [ + b.memberExpression(cname(), prototype_id), + b.callExpression(objectCreate_id, [ + b.memberExpression(freshSuper(), prototype_id), + ]), + ]) + ) + ); + + // 14.5.17 step 9, make sure the constructor's __proto__ is set to superClass + class_init_iife_body.unshift( + b.expressionStatement(b.callExpression(setPrototypeOf_id, [cname(), freshSuper()])) + ); + + class_init_iife_body.unshift( + b.expressionStatement(intrinsic(setConstructorKindDerived_id, [cname()])) + ); + } else { + class_init_iife_body.unshift( + b.expressionStatement(intrinsic(setConstructorKindBase_id, [cname()])) + ); + } + + class_init_iife_body.unshift(ctor_func); + + // make sure we return the function from our iife + class_init_iife_body.push(b.returnStatement(cname())); + + // (function (%super?) { ... }) + const iife_body = b.blockStatement(class_init_iife_body, n.loc ?? null); + return b.functionExpression( + b.identifier(`${n.id.name || "anonclass"}_iife`), + n.superClass ? [freshSuper()] : [], + iife_body + ); + } + + private gather_members(ast_class: NamedClass): { + properties: Map; + methods: Map; + sproperties: Map; + smethods: Map; + } { + const methods = new Map(); + const smethods = new Map(); + const properties = new Map(); + const sproperties = new Map(); + + for (const class_element of ast_class.body.body) { + const class_element_name = this.nameOfKey(class_element.key); + if (class_element.static && class_element_name === "prototype") + reportError( + SyntaxError, + 'Illegal method name "prototype" on static class member.', + this.filename, + class_element.loc ?? undefined + ); + + if (class_element.kind === "method" || class_element.kind === "constructor") { + // a method + const method_map = class_element.static ? smethods : methods; + if (method_map.has(class_element_name)) + reportError( + SyntaxError, + `method '${class_element_name}' has already been defined.`, + this.filename, + class_element.loc ?? undefined + ); + method_map.set(class_element_name, class_element); + } else if (class_element.kind === "get" || class_element.kind === "set") { + // an accessor property + const property_map = class_element.static ? sproperties : properties; + + // key non-computed accessors by NAME so a get/set pair for + // the same property shares one entry: keying by the key + // AST node put them in separate entries, and the emitted + // `{ n: {get}, n: {set} }` object literal lost the getter + const prop_key = class_element.computed ? class_element.key : class_element_name; + + let entry = property_map.get(prop_key); + if (!entry) { + entry = { computed: class_element.computed === true }; + property_map.set(prop_key, entry); + } + + if (entry[class_element.kind]) + reportError( + SyntaxError, + `a '${class_element.kind}' method for '${this.nameOfKey( + class_element.key + )}' has already been defined.`, + this.filename, + class_element.loc ?? undefined + ); + + if (class_element.kind === "set") { + const params = class_element.value.params; + const last_param = params[params.length - 1]; + if (last_param && last_param.type === "RestElement") + reportError( + SyntaxError, + "Setters are not allowed to have a rest", + this.filename, + last_param.loc ?? undefined + ); + } + + // XXX this doesn't work for properties where one accessor + // is computed and the other isn't... + if (entry.computed !== (class_element.computed === true)) + reportError( + Error, + "unsupported mismatch computed state for property accessors", + this.filename, + class_element.loc ?? undefined + ); + + entry[class_element.kind] = class_element; + } else { + reportError( + Error, + `unhandled class element kind '${class_element.kind}'`, + this.filename, + class_element.loc ?? undefined + ); + } + } + + return { properties, methods, sproperties, smethods }; + } + + private create_constructor( + ast_method: e.MethodDefinition, + ast_class: NamedClass + ): e.FunctionDeclaration { + // fresh id: ast_class.id is the outer let declarator's node + return b.functionDeclaration( + b.identifier(ast_class.id.name), + ast_method.value.params, + ast_method.value.body, + ast_method.value.defaults + ); + } + + private create_default_constructor(ast_class: NamedClass): e.MethodDefinition { + // splat args into the call to super's ctor if there's a superclass + const args_id = b.identifier("args"); + const functionBody = b.blockStatement( + ast_class.superClass + ? [ + b.expressionStatement( + intrinsic(constructSuperApply_id, [freshSuper(), args_id]) + ), + ] + : [] + ); + return b.methodDefinition( + constructor_id, + b.functionExpression(null, [b.restElement(args_id)], functionBody, []) + ); + } + + private nameOfKey(key: e.Expression): string { + return key.type === "Identifier" ? key.name : String((key as e.Literal).value); + } + + private create_proto_method( + ast_method: e.MethodDefinition, + ast_class: NamedClass + ): e.Statement { + const method_name = this.nameOfKey(ast_method.key); + const method_key = ast_method.computed ? ast_method.key : b.literal(method_name); + const method = b.functionExpression( + b.identifier(`${ast_class.id.name}:${method_name}`), + ast_method.value.params, + ast_method.value.body, + ast_method.value.defaults + ); + // b.functionExpression hardcodes generator: false — losing the + // flag here left `*method() {}` yields undesugared + method.generator = ast_method.value.generator; + + const Object_defineProperty = b.memberExpression(Object_id, defineProperty_id); + const defineProperty_args = b.objectExpression([ + b.property(value_id, method), + b.property(enumerable_id, b.literal(false)), + ]); + return b.expressionStatement( + b.callExpression(Object_defineProperty, [freshProto(), method_key, defineProperty_args]) + ); + } + + private create_static_method( + ast_method: e.MethodDefinition, + ast_class: NamedClass + ): e.Statement { + const method_name = this.nameOfKey(ast_method.key); + const method_key = ast_method.computed ? ast_method.key : b.literal(method_name); + const method = b.functionExpression( + ast_method.key.type === "Identifier" ? ast_method.key : null, + ast_method.value.params, + ast_method.value.body, + ast_method.value.defaults + ); + method.generator = ast_method.value.generator; + + const Object_defineProperty = b.memberExpression(Object_id, defineProperty_id); + const defineProperty_args = b.objectExpression([ + b.property(value_id, method), + b.property(enumerable_id, b.literal(false)), + ]); + return b.expressionStatement( + b.callExpression(Object_defineProperty, [ + b.identifier(ast_class.id.name), + method_key, + defineProperty_args, + ]) + ); + } + + private create_properties( + properties: Map, + ast_class: NamedClass, + are_static: boolean + ): e.Statement | null { + const propdescs: e.Property[] = []; + + properties.forEach((entry) => { + const accessors: e.Property[] = []; + let key: e.Expression | null = null; + + const getter = entry.get; + const setter = entry.set; + + // the map key is a name for non-computed accessors (so a + // get/set pair shares an entry); the emitted property key is + // the accessor's own key node + if (getter) { + accessors.push(b.property(get_id, getter.value)); + key = getter.key; + } + if (setter) { + accessors.push(b.property(set_id, setter.value)); + key = setter.key; + } + + propdescs.push( + b.property(key!, b.objectExpression(accessors), "init", entry.computed) + ); + }); + + if (propdescs.length === 0) return null; + + const propdescs_literal = b.objectExpression(propdescs); + + const target = are_static ? b.identifier(ast_class.id.name) : b.identifier("proto"); + + return b.expressionStatement( + b.callExpression(b.memberExpression(Object_id, defineProperties_id), [ + target, + propdescs_literal, + ]) + ); + } +} diff --git a/lib/passes/desugar-defaults.js b/lib/passes/desugar-defaults.js deleted file mode 100644 index 6b7139dc..00000000 --- a/lib/passes/desugar-defaults.js +++ /dev/null @@ -1,61 +0,0 @@ -// -// desugars -// -// function (a, b = a) { ... } -// -// to: -// -// function (a, b) { -// a = %getArg(0, undefined); -// b = %getArg(1, a); -// } -// - -import { reportError } from "../errors"; -import { argPresent_id, getArg_id } from "../common-ids"; -import { intrinsic } from "../echo-util"; -import { TransformPass } from "../node-visitor"; -import * as b from "../ast-builder"; - -export class DesugarDefaults extends TransformPass { - constructor(options, filename) { - super(options); - this.filename = filename; - } - - visitFunction(n) { - n = super.visitFunction(n); - - let prepends = []; - let seen_default = false; - - n.params.forEach((p, i) => { - let d = n.defaults[i]; - if (d) { - seen_default = true; - } else { - if (seen_default) { - reportError( - SyntaxError, - "Cannot specify non-default parameter after a default parameter", - this.filename, - p.loc - ); - } - d = b.undefinedLit(); - } - let let_decl = b.letDeclaration( - p, - intrinsic(getArg_id, [ - b.literal(i), - n.defaults[i] != null ? n.defaults[i] : b.undefinedLit(), - ]) - ); - let_decl.loc = n.body.loc; - prepends.push(let_decl); - }); - n.body.body = prepends.concat(n.body.body); - n.defaults = []; - return n; - } -} diff --git a/lib/passes/desugar-destructuring.js b/lib/passes/desugar-destructuring.js deleted file mode 100644 index a05fca99..00000000 --- a/lib/passes/desugar-destructuring.js +++ /dev/null @@ -1,228 +0,0 @@ -/* -*- Mode: js2; indent-tabs-mode: nil; tab-width: 4; js2-indent-offset: 4; js2-basic-offset: 4; -*- - * vim: set ts=4 sw=4 et tw=99 ft=js: - */ - -import { startGenerator, intrinsic } from "../echo-util"; -import { TransformPass } from "../node-visitor"; -import * as b from "../ast-builder"; -import { reportError } from "../errors"; -import { - Symbol_id, - iterator_id, - createIteratorWrapper_id, - getNextValue_id, - getRest_id, -} from "../common-ids"; - -let gen = startGenerator(); -let fresh = () => b.identifier(`%destruct_tmp${gen()}`); - -let Symbol_iterator = b.memberExpression(Symbol_id, iterator_id); - -// given an assignment { pattern } = id -// -function createObjectPatternBindings(id, pattern, bindings) { - for (let prop of pattern.properties) { - let memberexp = b.memberExpression(id, prop.key); - - if (prop.value.type === b.Identifier) { - if (prop.computed) { - bindings.push({ key: prop.value, value: memberexp }); - memberexp.computed = true; - } else bindings.push({ key: prop.value, value: memberexp }); - } else if (prop.value.type === b.ObjectPattern) { - bindings.push({ key: prop.key, value: memberexp }); - - createObjectPatternBindings(memberexp, prop.value, bindings); - } else if (prop.value.type === b.ArrayPattern) { - bindings.push({ key: prop.key, value: memberexp }); - - createArrayPatternBindingsUsingIterator(memberexp, prop.value, bindings); - } else { - throw new Error(`createObjectPatternBindings: prop.value.type = ${prop.value.type}`); - } - } -} - -function createArrayPatternBindingsUsingIterator(id, pattern, bindings) { - let seen_spread = false; - - // first off we create an iterator and wrapper for the rhs - let iter_id = fresh(); - let wrapper_id = fresh(); - bindings.push({ - key: iter_id, - value: b.callExpression(b.memberExpression(id, Symbol_iterator, true), []), - need_decl: true, - }); - bindings.push({ - key: wrapper_id, - value: intrinsic(createIteratorWrapper_id, [iter_id]), - need_decl: true, - }); - - for (let el of pattern.elements) { - if (seen_spread) - reportError(SyntaxError, "elements after spread element in array pattern", el.loc); - - if (el == null) { - bindings.push({ - key: fresh() /*unused*/, - value: b.callExpression(b.memberExpression(wrapper_id, getNextValue_id), []), - }); - } else if (el.type == b.Identifier) { - bindings.push({ - key: el, - value: b.callExpression(b.memberExpression(wrapper_id, getNextValue_id), []), - }); - } else if (el.type == b.ObjectPattern) { - let p_id = fresh(); - - bindings.push({ - key: p_id, - value: b.callExpression(b.memberExpression(wrapper_id, getNextValue_id), []), - }); - - createObjectPatternBindings(p_id, el, bindings); - } else if (el.type === b.ArrayPattern) { - let p_id = fresh(); - - bindings.push({ - key: p_id, - value: b.callExpression(b.memberExpression(wrapper_id, getNextValue_id), []), - }); - - createArrayPatternBindingsUsingIterator(p_id, el, bindings); - } else if (el.type === b.SpreadElement) { - bindings.push({ - key: el.argument, - value: b.callExpression(b.memberExpression(wrapper_id, getRest_id), []), - }); - seen_spread = true; - } else throw new Error(`createArrayPatternBindingsUsingIterator ${el.type}`); - } -} - -export class DesugarDestructuring extends TransformPass { - visitFunction(n) { - // we visit the formal parameters directly, rewriting - // them as tmp arg names and adding 'let' decls for the - // pattern identifiers at the top of the function's - // body. - let new_params = []; - let new_decls = []; - for (let p of n.params) { - let ptype = p.type; - if (ptype === b.ObjectPattern) { - let p_id = fresh(); - new_params.push(p_id); - let new_decl = b.letDeclaration(); - let bindings = []; - createObjectPatternBindings(p_id, p, bindings); - for (let binding of bindings) { - new_decl.declarations.push(b.variableDeclarator(binding.key, binding.value)); - } - new_decls.push(new_decl); - } else if (ptype === b.ArrayPattern) { - let p_id = fresh(); - new_params.push(p_id); - let new_decl = b.letDeclaration(); - let bindings = []; - createArrayPatternBindingsUsingIterator(p_id, p, bindings); - for (let binding of bindings) { - new_decl.declarations.push(b.variableDeclarator(binding.key, binding.value)); - } - new_decls.push(new_decl); - } else if (ptype === b.Identifier) { - // we just pass this along - new_params.push(p); - } else { - throw new Error( - `unhandled type of formal parameter in DesugarDestructuring ${ptype}` - ); - } - } - - n.body.body = new_decls.concat(n.body.body); - n.params = new_params; - n.body = this.visit(n.body); - return n; - } - - visitVariableDeclaration(n) { - let decls = []; - - for (let decl of n.declarations) { - if (decl.id.type === b.ObjectPattern) { - let obj_tmp_id = fresh(); - let bindings = []; - decls.push(b.variableDeclarator(obj_tmp_id, this.visit(decl.init))); - createObjectPatternBindings(obj_tmp_id, decl.id, bindings); - for (let binding of bindings) { - decls.push(b.variableDeclarator(binding.key, binding.value)); - } - } else if (decl.id.type === b.ArrayPattern) { - // create a fresh tmp and declare it - let array_tmp_id = fresh(); - let bindings = []; - decls.push(b.variableDeclarator(array_tmp_id, this.visit(decl.init))); - createArrayPatternBindingsUsingIterator(array_tmp_id, decl.id, bindings); - for (let binding of bindings) { - decls.push(b.variableDeclarator(binding.key, binding.value)); - } - } else if (decl.id.type === b.Identifier) { - decl.init = this.visit(decl.init); - decls.push(decl); - } else { - reportError( - Error, - `unhandled type of variable declaration in DesugarDestructuring ${decl.id.type}`, - this.filename, - n.loc - ); - } - } - n.declarations = decls; - return n; - } - - visitAssignmentExpression(n) { - if (n.left.type === b.ObjectPattern || n.left.type === b.ArrayPattern) { - if (n.operator !== "=") - reportError( - Error, - "cannot use destructuring with assignment operators other than =", - this.filename, - n.loc - ); - - let obj_tmp_id = fresh(); - let tmp_decl = b.letDeclaration(obj_tmp_id, this.visit(n.right)); - - let assignments = []; - let bindings = []; - if (n.left.type === b.ObjectPattern) - createObjectPatternBindings(obj_tmp_id, n.left, bindings); - else createArrayPatternBindingsUsingIterator(obj_tmp_id, n.left, bindings); - - for (let binding of bindings) { - if (binding.need_decl) { - assignments.push(b.letDeclaration(binding.key, binding.value)); - } else { - assignments.push( - b.expressionStatement( - b.assignmentExpression(binding.key, "=", binding.value) - ) - ); - } - } - - assignments.push(b.returnStatement(obj_tmp_id)); - - return b.callExpression( - b.functionExpression(null, [], b.blockStatement([tmp_decl, ...assignments])), - [] - ); - } else return super.visitAssignmentExpression(n); - } -} diff --git a/lib/passes/desugar-destructuring.ts b/lib/passes/desugar-destructuring.ts new file mode 100644 index 00000000..772a6b53 --- /dev/null +++ b/lib/passes/desugar-destructuring.ts @@ -0,0 +1,340 @@ +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: + */ + +import { startGenerator, intrinsic } from "../echo-util"; +import { TransformPass, VisitResult } from "../node-visitor"; +import * as b from "../ast-builder"; +import { reportError } from "../errors"; +import { + Symbol_id, + iterator_id, + createIteratorWrapper_id, + getNextValue_id, + getRest_id, +} from "../common-ids"; +import type * as e from "../estree"; + +const gen = startGenerator(); +const fresh = () => b.identifier(`%destruct_tmp${gen()}`); + +// note: value-position identifiers must be fresh AST nodes per use (the +// EIR scope analysis resolves references in a node-keyed map), so the +// Symbol.iterator member expression is minted per call site +function symbolIterator(): e.MemberExpression { + return b.memberExpression(b.identifier(Symbol_id.name), iterator_id); +} + +// one desugared binding: `key` receives `value`. need_decl bindings are +// synthesized temps; the rest are the pattern's own targets (declared in +// declaration position, assigned in assignment position). +interface Binding { + key: e.Identifier | e.Pattern; + value: e.Expression; + need_decl?: boolean; +} + +// bind `target` (an Identifier or a nested pattern) to `value`, applying +// the AssignmentPattern default `dflt` if present: +// let %dt = value, target = %dt === undefined ? dflt : %dt; +function bindTarget( + target: e.Pattern, + value: e.Expression, + dflt: e.Expression | null, + bindings: Binding[] +): void { + if (dflt) { + const dt = fresh(); + bindings.push({ key: dt, value: value, need_decl: true }); + value = b.conditionalExpression( + b.binaryExpression(b.identifier(dt.name), "===", b.undefinedLit()), + dflt, + b.identifier(dt.name) + ); + } + + if (target.type === "Identifier") { + bindings.push({ key: target, value: value }); + return; + } + + // a nested pattern: land the (possibly defaulted) value in a temp and + // recurse + const pt = fresh(); + bindings.push({ key: pt, value: value, need_decl: true }); + if (target.type === "ObjectPattern") + createObjectPatternBindings(b.identifier(pt.name), target, bindings); + else if (target.type === "ArrayPattern") + createArrayPatternBindingsUsingIterator(b.identifier(pt.name), target, bindings); + else throw new Error(`bindTarget: target.type = ${target.type}`); +} + +// given an assignment { pattern } = id +// +function createObjectPatternBindings( + id: e.Identifier, + pattern: e.ObjectPattern, + bindings: Binding[] +): void { + for (const prop of pattern.properties) { + const memberexp = b.memberExpression(id, prop.key); + if (prop.computed) memberexp.computed = true; + + let target = prop.value as e.Pattern; + let dflt: e.Expression | null = null; + if (target.type === "AssignmentPattern") { + dflt = target.right; + target = target.left; + } + + bindTarget(target, memberexp, dflt, bindings); + } +} + +function createArrayPatternBindingsUsingIterator( + id: e.Identifier, + pattern: e.ArrayPattern, + bindings: Binding[] +): void { + let seen_spread = false; + + // first off we create an iterator and wrapper for the rhs + const iter_id = fresh(); + const wrapper_id = fresh(); + bindings.push({ + key: iter_id, + value: b.callExpression(b.memberExpression(id, symbolIterator(), true), []), + need_decl: true, + }); + bindings.push({ + key: wrapper_id, + value: intrinsic(createIteratorWrapper_id, [iter_id]), + need_decl: true, + }); + + const nextValue = () => + b.callExpression(b.memberExpression(b.identifier(wrapper_id.name), getNextValue_id), []); + + for (const el of pattern.elements) { + if (seen_spread) + reportError( + SyntaxError, + "elements after spread element in array pattern", + "", + el && el.loc ? el.loc : undefined + ); + + if (el == null) { + bindings.push({ key: fresh() /*unused*/, value: nextValue() }); + } else if (el.type === "SpreadElement" || el.type === "RestElement") { + // declaration-position rests parse as SpreadElement, + // assignment-position ones as RestElement + bindings.push({ + key: el.argument as e.Pattern, + value: b.callExpression( + b.memberExpression(b.identifier(wrapper_id.name), getRest_id), + [] + ), + }); + seen_spread = true; + } else { + let target: e.Pattern = el; + let dflt: e.Expression | null = null; + if (target.type === "AssignmentPattern") { + dflt = target.right; + target = target.left; + } + bindTarget(target, nextValue(), dflt, bindings); + } + } +} + +export class DesugarDestructuring extends TransformPass { + // a pattern (or member-expression) loop head desugars to a fresh + // identifier head plus a binding statement at the top of the body: + // + // for (let [a, b] of xs) body => for (let %t of xs) { let [a, b] = %t; body } + // for (o.x of xs) body => for (let %t of xs) { o.x = %t; body } + // + // the inner statement then desugars through the ordinary + // declaration/assignment paths. body-scoped `let`s are fresh per + // iteration, preserving per-iteration capture semantics. + private desugarForHead(n: e.ForOfStatement | e.ForInStatement): VisitResult { + const head = n.left; + let bindStmt: VisitResult = null; + if (head.type === "VariableDeclaration") { + const d = head.declarations[0]!; + if (head.declarations.length === 1 && d.id.type !== "Identifier") { + const tmp = fresh(); + const inner = b.variableDeclaration(head.kind, d.id, b.identifier(tmp.name)); + bindStmt = this.visit(inner); + const newHead = b.letDeclaration(tmp, null); + // strip the placeholder init: a for-of/for-in head + // declaration has no initializer + newHead.declarations[0]!.init = null; + n.left = newHead; + } + } else if (head.type !== "Identifier") { + // ObjectPattern/ArrayPattern assignment form, or a member + // expression target + const tmp = fresh(); + const assign = b.expressionStatement( + b.assignmentExpression(head, "=", b.identifier(tmp.name)) + ); + bindStmt = this.visit(assign); + const newHead = b.letDeclaration(tmp, null); + newHead.declarations[0]!.init = null; + n.left = newHead; + } + n.right = this.visitAs(n.right); + n.body = this.visitAs(n.body); + if (bindStmt) { + const stmts = (Array.isArray(bindStmt) ? bindStmt : [bindStmt]) as e.Statement[]; + n.body = b.blockStatement(stmts.concat([n.body])); + } + return n; + } + + override visitForOf(n: e.ForOfStatement): VisitResult { + return this.desugarForHead(n); + } + + override visitForIn(n: e.ForInStatement): VisitResult { + return this.desugarForHead(n); + } + + // catch ({ message }) { ... } => catch (%t) { let { message } = %t; ... } + override visitCatchClause(n: e.CatchClause): VisitResult { + if (n.param && n.param.type !== "Identifier") { + const tmp = fresh(); + const bindDecl = this.visitAs( + b.letDeclaration(n.param, b.identifier(tmp.name)) + ); + n.param = tmp; + n.body = this.visitAs(n.body); + n.body.body.unshift(bindDecl); + return n; + } + return super.visitCatchClause(n); + } + + override visitFunction(n: e.Function): VisitResult { + // we visit the formal parameters directly, rewriting + // them as tmp arg names and adding 'let' decls for the + // pattern identifiers at the top of the function's + // body. + const new_params: e.Pattern[] = []; + const new_decls: e.VariableDeclaration[] = []; + for (const p of n.params) { + if (p.type === "ObjectPattern" || p.type === "ArrayPattern") { + const p_id = fresh(); + new_params.push(p_id); + const bindings: Binding[] = []; + if (p.type === "ObjectPattern") createObjectPatternBindings(p_id, p, bindings); + else createArrayPatternBindingsUsingIterator(p_id, p, bindings); + const new_decl = b.variableDeclaration( + "let", + bindings.map((binding) => b.variableDeclarator(binding.key, binding.value)) + ); + new_decls.push(new_decl); + } else if (p.type === "Identifier") { + // we just pass this along + new_params.push(p); + } else if (p.type === "RestElement" && p.argument.type === "Identifier") { + // a trailing ...rest stays in place (EIR handles it natively) + new_params.push(p); + } else { + throw new Error( + `unhandled type of formal parameter in DesugarDestructuring ${p.type}` + ); + } + } + + // expression-bodied arrows have no statement list: writing + // n.body.body here used to clobber the body of `() => () => ...` + // (the inner arrow's body field) with [undefined]. wrap in a + // block only when there are decls to prepend. + if (n.body.type === "BlockStatement") { + n.body.body = (new_decls as e.Statement[]).concat(n.body.body); + } else if (new_decls.length > 0) { + n.body = b.blockStatement( + (new_decls as e.Statement[]).concat([b.returnStatement(n.body)]) + ); + n.expression = false; + } + n.params = new_params; + n.body = this.visitAs(n.body); + return n; + } + + override visitVariableDeclaration(n: e.VariableDeclaration): VisitResult { + const decls: e.VariableDeclarator[] = []; + + for (const decl of n.declarations) { + if (decl.id.type === "ObjectPattern" || decl.id.type === "ArrayPattern") { + const tmp_id = fresh(); + const bindings: Binding[] = []; + decls.push(b.variableDeclarator(tmp_id, this.visitNullable(decl.init ?? null))); + if (decl.id.type === "ObjectPattern") + createObjectPatternBindings(tmp_id, decl.id, bindings); + else createArrayPatternBindingsUsingIterator(tmp_id, decl.id, bindings); + for (const binding of bindings) { + decls.push(b.variableDeclarator(binding.key, binding.value)); + } + } else if (decl.id.type === "Identifier") { + decl.init = this.visitNullable(decl.init ?? null); + decls.push(decl); + } else { + reportError( + Error, + `unhandled type of variable declaration in DesugarDestructuring ${decl.id.type}`, + this.filename, + n.loc ?? undefined + ); + } + } + n.declarations = decls; + return n; + } + + override visitAssignmentExpression(n: e.AssignmentExpression): VisitResult { + if (n.left.type === "ObjectPattern" || n.left.type === "ArrayPattern") { + if (n.operator !== "=") + reportError( + Error, + "cannot use destructuring with assignment operators other than =", + this.filename, + n.loc ?? undefined + ); + + const obj_tmp_id = fresh(); + const tmp_decl = b.letDeclaration(obj_tmp_id, this.visitAs(n.right)); + + const assignments: e.Statement[] = []; + const bindings: Binding[] = []; + if (n.left.type === "ObjectPattern") + createObjectPatternBindings(obj_tmp_id, n.left, bindings); + else createArrayPatternBindingsUsingIterator(obj_tmp_id, n.left, bindings); + + for (const binding of bindings) { + if (binding.need_decl) { + assignments.push(b.letDeclaration(binding.key, binding.value)); + } else { + assignments.push( + b.expressionStatement( + b.assignmentExpression(binding.key, "=", binding.value) + ) + ); + } + } + + assignments.push(b.returnStatement(b.identifier(obj_tmp_id.name))); + + return b.callExpression( + b.functionExpression(null, [], b.blockStatement([tmp_decl, ...assignments])), + [] + ); + } + return super.visitAssignmentExpression(n); + } +} diff --git a/lib/passes/desugar-for-of.js b/lib/passes/desugar-for-of.js deleted file mode 100644 index b188df8b..00000000 --- a/lib/passes/desugar-for-of.js +++ /dev/null @@ -1,94 +0,0 @@ -/* -*- Mode: js2; indent-tabs-mode: nil; tab-width: 4; js2-indent-offset: 4; js2-basic-offset: 4; -*- - * vim: set ts=4 sw=4 et tw=99 ft=js: - */ -// -// desugars -// -// for (let x of a) { ... } -// -// to: -// -// { -// %forof = a[Symbol.iterator](); -// while (!(%iter_next = %forof.next()).done) { -// let x = %iter_next.value; -// { ... } -// } -// } - -import * as b from "../ast-builder"; -import { TransformPass } from "../node-visitor"; -import { startGenerator } from "../echo-util"; -import { Stack } from "../stack-es6"; -import { Symbol_id, iterator_id, value_id, next_id, done_id } from "../common-ids"; - -let forofgen = startGenerator(); -let freshForOf = function (ident) { - return `%forof${ident}_${forofgen()}`; -}; - -export class DesugarForOf extends TransformPass { - constructor(options) { - super(options); - this.function_stack = new Stack(); - } - - visitFunction(n) { - this.function_stack.push(n); - let rv = super.visitFunction(n); - this.function_stack.pop(); - return rv; - } - - visitForOf(n) { - n.left = this.visit(n.left); - n.right = this.visit(n.right); - n.body = this.visit(n.body); - - let iterable_tmp = freshForOf("tmp"); - let iter_name = freshForOf("iter"); - let iter_next_name = freshForOf("next"); - - let iterable_id = b.identifier(iterable_tmp); - let iter_id = b.identifier(iter_name); - let iter_next_id = b.identifier(iter_next_name); - - let tmp_iterable_decl = b.letDeclaration(iterable_id, n.right); - - let Symbol_iterator = b.memberExpression(Symbol_id, iterator_id); - let get_iterator_stmt = b.letDeclaration( - iter_id, - b.callExpression(b.memberExpression(iterable_id, Symbol_iterator, true), []) - ); - - let loop_iter_stmt; - - if (n.left.type === b.VariableDeclaration) - loop_iter_stmt = b.letDeclaration( - n.left.declarations[0].id, // can there be more than 1? - b.memberExpression(iter_next_id, value_id) - ); - else - loop_iter_stmt = b.expressionStatement( - b.assignmentExpression(n.left, "=", b.memberExpression(iter_next_id, value_id)) - ); - - let next_decl = b.letDeclaration(iter_next_id, b.undefinedLit()); - - let not_done = b.unaryExpression( - "!", - b.memberExpression( - b.assignmentExpression( - iter_next_id, - "=", - b.callExpression(b.memberExpression(iter_id, next_id)) - ), - done_id - ) - ); - - let while_stmt = b.whileStatement(not_done, b.blockStatement([loop_iter_stmt, n.body])); - - return b.blockStatement([tmp_iterable_decl, get_iterator_stmt, next_decl, while_stmt]); - } -} diff --git a/lib/passes/desugar-generator-functions.js b/lib/passes/desugar-generator-functions.js deleted file mode 100644 index 3e853df7..00000000 --- a/lib/passes/desugar-generator-functions.js +++ /dev/null @@ -1,70 +0,0 @@ -/* -*- Mode: js2; indent-tabs-mode: nil; tab-width: 4; js2-indent-offset: 4; js2-basic-offset: 4; -*- - * vim: set ts=4 sw=4 et tw=99 ft=js: - */ -// this pass converts all generator functions like this: -// -// function* foo() { -// yield 1; -// yield 2; -// yield 3; -// } -// -// into this: -// -// function foo() { -// // arrow function so `this` is bound -// let %gen = %makeGenerator(() => { -// %generatorYield(%gen, 1); -// %generatorYield(%gen, 2); -// %generatorYield(%gen, 3); -// } -// return %gen; -// } -// - -import { TransformPass } from "../node-visitor"; -import * as b from "../ast-builder"; -import { makeGenerator_id, generatorYield_id } from "../common-ids"; -import { intrinsic, startGenerator } from "../echo-util"; -import { reportError, reportWarning } from "../errors"; - -export class DesugarGeneratorFunctions extends TransformPass { - constructor(options) { - super(options); - this.mapping = []; - this.genGen = startGenerator(); - this.yieldGen = startGenerator(); - } - - visitFunction(n) { - if (n.generator) this.mapping.unshift(b.identifier(`%_gen_${this.genGen()}`)); - n = super.visitFunction(n); - if (n.generator) { - let old_body = n.body; - n.body = b.blockStatement([ - b.letDeclaration( - this.mapping[0], - intrinsic(makeGenerator_id, [b.arrowFunctionExpression([], old_body)]) - ), - b.returnStatement(this.mapping[0]), - ]); - n.generator = false; - } - this.mapping.shift(); - return n; - } - - visitYield(n) { - n.argument = this.visit(n.argument); - if (n.delegate) { - let yield_id = b.identifier(`%_yield_${this.genGen()}`); - return b.forOfStatement( - b.letDeclaration(yield_id, null), - n.argument, - b.blockStatement([intrinsic(generatorYield_id, [this.mapping[0], yield_id])]) - ); - } else { - return intrinsic(generatorYield_id, [this.mapping[0], n.argument]); - } - } -} diff --git a/lib/passes/desugar-generator-functions.ts b/lib/passes/desugar-generator-functions.ts new file mode 100644 index 00000000..7bfe32ec --- /dev/null +++ b/lib/passes/desugar-generator-functions.ts @@ -0,0 +1,124 @@ +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: + */ + +// coroutine-style generator desugar: +// +// function* gen() { +// yield 1; +// yield 2; +// yield 3; +// } +// +// becomes +// +// function gen() { +// let %gen = %makeGenerator(() => { +// %generatorYield(%gen, 1); +// %generatorYield(%gen, 2); +// %generatorYield(%gen, 3); +// }); +// return %gen; +// } +// +// the body closure runs on its own stack (runtime ucontext switch); the +// wrapper's catch converts the runtime's .return() sentinel into a +// normal return (see _ejs_Generator_prototype_return). + +import { TransformPass, VisitResult } from "../node-visitor"; +import * as b from "../ast-builder"; +import { intrinsic, startGenerator } from "../echo-util"; +import { + makeGenerator_id, + generatorYield_id, + generatorIsReturnSentinel_id, + generatorReturnValue_id, +} from "../common-ids"; +import type * as e from "../estree"; + +export class DesugarGeneratorFunctions extends TransformPass { + // innermost generator's %gen identifier first; functions nest + private mapping: e.Identifier[] = []; + private genGen = startGenerator(); + + override visitFunction(n: e.Function): VisitResult { + if (n.generator) this.mapping.unshift(b.identifier(`%_gen_${this.genGen()}`)); + super.visitFunction(n); + if (n.generator) { + const gen_id = this.mapping[0]!; + // the body wraps in a catch that converts the runtime's + // .return() sentinel into a normal return: gen.return(v) + // resumes the suspended yield by throwing the sentinel, so + // finally blocks run, and this outermost catch completes the + // generator with v + const exc_id = b.identifier(`%_genexc_${this.genGen()}`); + const old_body = b.blockStatement([ + b.tryStatement( + n.body as e.BlockStatement, + [ + b.catchClause( + exc_id, + b.blockStatement([ + b.ifStatement( + intrinsic(generatorIsReturnSentinel_id, [ + b.identifier(exc_id.name), + ]), + b.returnStatement( + intrinsic(generatorReturnValue_id, [ + b.identifier(gen_id.name), + ]) + ), + b.throwStatement(b.identifier(exc_id.name)) + ), + ]) + ), + ], + null + ), + ]); + n.body = b.blockStatement([ + b.letDeclaration( + gen_id, + intrinsic(makeGenerator_id, [b.arrowFunctionExpression([], old_body)]) + ), + b.returnStatement(b.identifier(gen_id.name)), + ]); + n.generator = false; + } + this.mapping.shift(); + return n; + } + + // yield* x → for (let %_yield of x) %generatorYield(%gen, %_yield); + // (n.argument must already be visited) + private delegateLoop(n: e.YieldExpression): e.ForOfStatement { + const yield_id = b.identifier(`%_yield_${this.genGen()}`); + return b.forOfStatement( + b.letDeclaration(yield_id, null), + n.argument!, + b.blockStatement([ + b.expressionStatement( + intrinsic(generatorYield_id, [this.mapping[0]!, yield_id]) + ), + ]) + ); + } + + // statement-position yield* replaces the whole ExpressionStatement + // with the for-of loop, keeping the AST well-formed + override visitExpressionStatement(n: e.ExpressionStatement): VisitResult { + if (n.expression.type === "YieldExpression" && n.expression.delegate) { + n.expression.argument = this.visitNullable(n.expression.argument); + return this.delegateLoop(n.expression); + } + return super.visitExpressionStatement(n); + } + + override visitYield(n: e.YieldExpression): VisitResult { + n.argument = this.visitNullable(n.argument); + if (n.delegate) { + return this.delegateLoop(n); + } + return intrinsic(generatorYield_id, [this.mapping[0]!, n.argument ?? b.undefinedLit()]); + } +} diff --git a/lib/passes/desugar-import-export.js b/lib/passes/desugar-import-export.js deleted file mode 100644 index 46846eeb..00000000 --- a/lib/passes/desugar-import-export.js +++ /dev/null @@ -1,240 +0,0 @@ -/* -*- Mode: js2; indent-tabs-mode: nil; tab-width: 4; js2-indent-offset: 4; js2-basic-offset: 4; -*- - * vim: set ts=4 sw=4 et tw=99 ft=js: - */ - -import { startGenerator, intrinsic } from "../echo-util"; -import * as b from "../ast-builder"; -import { reportError, reportWarning } from "../errors"; -import { moduleGetSlot_id, moduleSetSlot_id, moduleGetExotic_id } from "../common-ids"; -import { TransformPass } from "../node-visitor"; - -let importGen = startGenerator(); -function freshId(prefix) { - return b.identifier(`%${prefix}_${importGen()}`); -} - -export class DesugarImportExport extends TransformPass { - constructor(options, filename, allModules) { - super(options); - this.allModules = allModules; - this.filename = filename; - } - - visitFunction(n) { - if (!n.toplevel) return n; - - this.exports = []; - this.batch_exports = []; - - return super.visitFunction(n); - } - - visitImportDeclaration(n) { - if (n.specifiers.length === 0) { - // no specifiers, it's of the form: import from "foo" - // don't waste a decl for this type - return b.expressionStatement(intrinsic(moduleGetExotic_id, [n.source_path])); - } - - let import_decls = b.letDeclaration(); - let module = this.allModules.get(n.source_path.value); - - for (let spec of n.specifiers) { - if (spec.type === b.ImportDefaultSpecifier) { - // - // let ${spec.local} = %import_decl.default - // - if (!module.hasDefaultExport()) - reportError( - ReferenceError, - `module '${n.source_path.value}' doesn't have default export`, - this.filename, - n.loc - ); - - import_decls.declarations.push( - b.variableDeclarator( - spec.local, - intrinsic(moduleGetSlot_id, [n.source_path, b.literal("default")]) - ) - ); - } else if (spec.type === b.ImportSpecifier) { - // - // let ${spec.local} = %import_decl.#{spec.imported} - // - if (!module.exports.has(spec.imported.name)) - reportError( - ReferenceError, - `module '${n.source_path.value}' doesn't export '${spec.imported.name}'`, - this.filename, - spec.imported.loc - ); - import_decls.declarations.push( - b.variableDeclarator( - spec.local, - intrinsic(moduleGetSlot_id, [n.source_path, b.literal(spec.imported.name)]) - ) - ); - } else if (spec.type === b.ImportNamespaceSpecifier) { - // let #{spec.name} = %import_decl - import_decls.declarations.push( - b.variableDeclarator(spec.local, intrinsic(moduleGetExotic_id, [n.source_path])) - ); - } else { - reportError( - Error, - `unknown import specifier type ${spec.type}`, - this.filename, - n.loc - ); - } - } - return import_decls; - } - - visitExportDefaultDeclaration(n) { - // export default = ...; - // - return b.expressionStatement( - intrinsic(moduleSetSlot_id, [ - b.literal(this.filename), - b.literal("default"), - this.visit(n.declaration), - ]) - ); - } - - visitExportAllDeclaration(n) { - let import_tmp = freshId("import"); - let export_stuff = [ - b.letDeclaration(import_tmp, intrinsic(moduleGetExotic_id, [n.source_path])), - ]; - - this.batch_exports.push({ source: import_tmp, specifiers: [] }); - } - - visitExportNamedDeclaration(n) { - if (n.source) { - // export { ... } from "foo" - - // import the module regardless - let import_tmp = freshId("import"); - let export_stuff = [ - b.letDeclaration(import_tmp, intrinsic(moduleGetExotic_id, [n.source_path])), - ]; - - for (let spec of n.specifiers) { - if (!this.allModules.get(n.source_path.value).exports.has(spec.local.name)) - reportError( - ReferenceError, - `module '${n.source_path.value}' doesn't export '${spec.exported.name}'`, - this.filename, - spec.local.loc - ); - - export_stuff.push( - b.expressionStatement( - intrinsic(moduleSetSlot_id, [ - b.literal(this.filename), - b.literal(spec.exported.name), - b.memberExpression(import_tmp, spec.local), - ]) - ) - ); - } - return export_stuff; - } - - if (!n.declaration) { - // export { ... } - let export_stuff = []; - for (let spec of n.specifiers) { - export_stuff.push( - b.expressionStatement( - intrinsic(moduleSetSlot_id, [ - b.literal(this.filename), - b.literal(spec.exported.name), - spec.local, - ]) - ) - ); - } - return export_stuff; - } - - // export function foo () { ... } - if (n.declaration.type === b.FunctionDeclaration) { - this.exports.push({ id: n.declaration.id }); - - // we're going to pass it to the moduleSetSlot intrinsic, so it needs to be an expression (or else escodegen freaks out) - n.declaration.type = b.FunctionExpression; - return b.expressionStatement( - intrinsic(moduleSetSlot_id, [ - b.literal(this.filename), - b.literal(n.declaration.id.name), - this.visit(n.declaration), - ]) - ); - } - - // export class Foo () { ... } - if (n.declaration.type === b.ClassDeclaration) { - this.exports.push({ id: n.declaration.id }); - - n.declaration.type = b.ClassExpression; - return b.expressionStatement( - intrinsic(moduleSetSlot_id, [ - b.literal(this.filename), - b.literal(n.declaration.id.name), - this.visit(n.declaration), - ]) - ); - } - - // export let foo = bar; - if (n.declaration.type === b.VariableDeclaration) { - let export_defines = []; - for (let decl of n.declaration.declarations) { - this.exports.push({ id: decl.id }); - export_defines.push( - b.expressionStatement( - intrinsic(moduleSetSlot_id, [ - b.literal(this.filename), - b.literal(decl.id.name), - this.visit(decl.init), - ]) - ) - ); - } - return export_defines; - } - - // export foo = bar; - if (n.declaration.type === b.VariableDeclarator) { - this.exports.push({ id: n.declaration.id }); - return b.expressionStatement( - intrinsic(moduleSetSlot_id, [ - b.literal(this.filename), - b.literal(n.declaration.id.name), - this.visit(n.declaration), - ]) - ); - } - - reportError( - SyntaxError, - `Unsupported type of export declaration ${n.declaration.type}`, - this.filename, - n.loc - ); - } - - visitModuleDeclaration(n) { - // this isn't quite right. I believe this form creates - // a new instance and puts new properties on it that - // map to the module, instead of just returning the - // module object. - let init = intrinsic(moduleGetExotic_id, [n.source_path]); - return b.letDeclaration(n.id, init); - } -} diff --git a/lib/passes/desugar-let-loopvars.js b/lib/passes/desugar-let-loopvars.js deleted file mode 100644 index 4226398e..00000000 --- a/lib/passes/desugar-let-loopvars.js +++ /dev/null @@ -1,141 +0,0 @@ -// we have a loop that looks like: -// -// for (let x = ...; $test; $update) { -// /* body */ -// } -// -// we desugar this to: -// -// for (var %loop_x = ...; $test(with x replaced with %loop_x); $update(with x replaced with %loop_x) { -// let x = %loop_x; -// try { -// /* body */ -// } -// finally { -// %loop_x = x; -// } -// } - -import * as b from "../ast-builder"; -import { Stack } from "../stack-es6"; -import { shallow_copy_object, startGenerator } from "../echo-util"; -import { TransformPass } from "../node-visitor"; - -let hasOwn = Object.prototype.hasOwnProperty; - -let vargen = startGenerator(); -function freshLoopVar(ident) { - return `%loop_${ident}_${vargen()}`; -} - -export class DesugarLetLoopVars extends TransformPass { - constructor(options, filename) { - super(options); - this.filename = filename; - } - - visitFor(n) { - // if the loop looks like: `for (; ...)` there's nothing for us to do - if (!n.init) return n; - - // if the loop looks like: `for (var i = 0; ...)` or `for (i = 0; ...)` there's nothing for us to do - if (n.init.type !== b.VariableDeclaration || n.init.kind !== "let") return n; - - n.init.kind = "var"; - - let mappings = Object.create(null); - - for (let decl of n.init.declarations) { - let loopvar = b.identifier(freshLoopVar(decl.id.name)); - mappings[decl.id.name] = loopvar; - decl.id = loopvar; - } - - let assignments = []; - let new_body = b.blockStatement(); - - for (let loopvar in mappings) { - // this gives us the "let x = %loop_x" - // assignments, so get our fresh binding per - // loop iteration - new_body.body.push( - b.variableDeclaration("let", b.identifier(loopvar), mappings[loopvar]) - ); - - // and this gives us the assignment we put in - // the finally block to capture changes made to - // the loop variable in the body - assignments.push( - b.expressionStatement( - b.assignmentExpression(mappings[loopvar], "=", b.identifier(loopvar)) - ) - ); - } - - new_body.body.push(b.tryStatement(n.body, [], b.blockStatement(assignments))); - - let remap = new RemapIdentifiers(this.options, this.filename, mappings); - n.test = remap.visit(n.test); - n.update = remap.visit(n.update); - - n.body = this.visit(new_body); - - return n; - } -} - -class RemapIdentifiers extends TransformPass { - constructor(options, filename, initial_mapping) { - super(options); - this.filename = filename; - this.mappings = new Stack(initial_mapping); - } - - visitBlock(n) { - // clone the mapping and push it onto the stack - this.mappings.push(shallow_copy_object(this.currentMapping())); - super.visitBlock(n); - this.mappings.pop(); - return n; - } - - visitVariableDeclarator(n) { - // if the variable's name exists in the mapping clear it out - this.currentMapping()[n.id.name] = null; - } - - visitObjectPattern(n) { - for (let prop of n.properties) this.currentMapping()[prop.key] = null; - super.visitObjectPattern(n); - } - - visitCatchClause(n) { - this.mappings.push(shallow_copy_object(this.currentMapping())); - this.currentMapping()[n.param.name] = null; - super.visitCatchClause(n); - this.mappings.pop(); - return n; - } - - visitFunction(n) { - if (n.id) this.currentMapping()[n.id.name] = null; - - this.mappings.push(shallow_copy_object(this.currentMapping())); - if (n.rest) this.currentMapping()[n.rest.name] = null; - super.visitFunction(n); - this.mappings.pop(); - return n; - } - - visitIdentifier(n) { - if (hasOwn.call(this.currentMapping(), n.name)) { - let mapped = this.currentMapping()[n.name]; - if (mapped) return mapped; - } - return n; - } - - currentMapping() { - return this.mappings.depth > 0 ? this.mappings.top : Object.create(null); - } -} diff --git a/lib/passes/desugar-metaproperties.js b/lib/passes/desugar-metaproperties.ts similarity index 60% rename from lib/passes/desugar-metaproperties.js rename to lib/passes/desugar-metaproperties.ts index e7e54bd8..11cfb83e 100644 --- a/lib/passes/desugar-metaproperties.js +++ b/lib/passes/desugar-metaproperties.ts @@ -1,32 +1,32 @@ -/* -*- Mode: js2; indent-tabs-mode: nil; tab-width: 4; js2-indent-offset: 4; js2-basic-offset: 4; -*- - * vim: set ts=4 sw=4 et tw=99 ft=js: +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: */ import { reportError } from "../errors"; -import { TransformPass } from "../node-visitor"; +import { TransformPass, VisitResult } from "../node-visitor"; import { getNewTarget_id } from "../common-ids"; import { intrinsic } from "../echo-util"; -import * as b from "../ast-builder"; +import type * as e from "../estree"; export class DesugarMetaProperties extends TransformPass { - visitAssignmentExpression(n) { - if (n.left.type === b.MetaProperty) + override visitAssignmentExpression(n: e.AssignmentExpression): VisitResult { + if (n.left.type === "MetaProperty") reportError( SyntaxError, `'${n.left.meta}.${n.left.property}' not permitted on left hand side of assignment`, this.filename, - n.left.loc + n.left.loc ?? undefined ); return super.visitAssignmentExpression(n); } - visitMetaProperty(n) { + override visitMetaProperty(n: e.MetaProperty): VisitResult { if (n.meta === "new" && n.property === "target") return intrinsic(getNewTarget_id, []); reportError( SyntaxError, `unknown meta property '${n.meta}.${n.property}'`, this.filename, - n.loc + n.loc ?? undefined ); } } diff --git a/lib/passes/desugar-rest-parameters.js b/lib/passes/desugar-rest-parameters.js deleted file mode 100644 index e7810f56..00000000 --- a/lib/passes/desugar-rest-parameters.js +++ /dev/null @@ -1,68 +0,0 @@ -/* -*- Mode: js2; indent-tabs-mode: nil; tab-width: 4; js2-indent-offset: 4; js2-basic-offset: 4; -*- - * vim: set ts=4 sw=4 et tw=99 ft=js: - */ - -// -// convert from: -// -// function name (arg1, arg2, arg3, ...rest) { -// // body -// } -// -// to: -// -// function name (arg1, arg2, arg3) { -// let rest = %arrayFromRest('rest', 3); -// // body -// } -// - -import { reportError } from "../errors"; -import { TransformPass } from "../node-visitor"; -import { arrayFromRest_id } from "../common-ids"; -import * as b from "../ast-builder"; - -export class DesugarRestParameters extends TransformPass { - visitProperty(n) { - if (n.kind !== "set") { - return super.visitProperty(n); - } - if (n.value.params.length > 0) { - let last_param = n.value.params[n.value.params.length - 1]; - if (last_param.type == b.RestElement) - reportError( - SyntaxError, - "Setters aren't allowed to have a rest", - this.filename, - last_param.loc - ); - } - n.value = super.visit(n.value); - return n; - } - - visitFunction(n) { - n = super.visitFunction(n); - if (n.params.length > 0 && n.params[n.params.length - 1].type == b.RestElement) { - let rest_argument = n.params[n.params.length - 1].argument; - n.params.pop(); - if (rest_argument.type !== b.Identifier) - reportError( - Error, - "we assume rest elements are of the form: ...Identifier", - this.filename, - rest_argument.argument.loc - ); - let rest_name = rest_argument.name; - let rest_declaration = b.letDeclaration( - b.identifier(rest_argument.name), - b.callExpression(arrayFromRest_id, [ - b.literal(rest_argument.name), - b.literal(n.params.length), - ]) - ); - n.body.body.unshift(rest_declaration); - } - return n; - } -} diff --git a/lib/passes/desugar-spread.js b/lib/passes/desugar-spread.js deleted file mode 100644 index bde31be9..00000000 --- a/lib/passes/desugar-spread.js +++ /dev/null @@ -1,139 +0,0 @@ -/* -*- Mode: js2; indent-tabs-mode: nil; tab-width: 4; js2-indent-offset: 4; js2-basic-offset: 4; -*- - * vim: set ts=4 sw=4 et tw=99 ft=js: - */ - -// -// desugars -// -// [1, 2, ...foo, 3, 4] -// -// o.foo(1, 2, ...foo, 3, 4) -// -// to: -// -// %arrayFromSpread([1, 2], foo, [3, 4]) -// -// o.foo.apply(o, %arrayFromSpread([1, 2], foo, [3, 4]) -// - -import { TransformPass } from "../node-visitor"; -import * as b from "../ast-builder"; -import { intrinsic, is_intrinsic } from "../echo-util"; -import { arrayFromSpread_id, apply_id } from "../common-ids"; - -export class DesugarSpread extends TransformPass { - visitArrayExpression(n) { - n = super.visitArrayExpression(n); - let needs_desugaring = false; - for (let el of n.elements) { - if (el && el.type === b.SpreadElement) { - needs_desugaring = true; - break; - } - } - - if (!needs_desugaring) return n; - - let new_args = []; - let current_elements = []; - for (let el of n.elements) { - if (el && el.type === b.SpreadElement) { - if (current_elements.length === 0) { - // just push the spread argument into the new args - new_args.push(el.argument); - } else { - // push the current_elements as an array literal, then the spread. - // also reset current_elements to [] - new_args.push(b.arrayExpression(current_elements)); - new_args.push(el.argument); - current_elements = []; - } - } else { - current_elements.push(el); - } - } - if (current_elements.length > 0) new_args.push(b.arrayExpression(current_elements)); - - // check to see if we've just created an array of nothing but array literals, and flatten them all - // into one and get rid of the spread altogether - let all_arrays = true; - for (let a of new_args) { - if (a.type !== b.ArrayExpression) all_arrays = false; - } - - if (all_arrays) { - let na = []; - for (let a of new_args) na = na.concat(a.elements); - n.elements = na; - return n; - } else { - return intrinsic(arrayFromSpread_id, new_args); - } - } - - visitCallExpression(n) { - n = super.visitCallExpression(n); - let needs_desugaring = false; - for (let el of n.arguments) { - if (el.type === b.SpreadElement) { - needs_desugaring = true; - break; - } - } - - if (!needs_desugaring) return n; - - let new_args = []; - let current_elements = []; - for (let el of n.arguments) { - if (is_intrinsic(el, "%arrayFromSpread")) { - // flatten spreads - new_args.concat(el.arguments); - } else if (el.type === b.SpreadElement) { - if (current_elements.length === 0) { - // just push the spread argument into the new args - new_args.push(el.argument); - } else { - // push the current_elements as an array literal, then the spread. - // also reset current_elements to [] - new_args.push(b.arrayExpression(current_elements)); - new_args.push(el.argument); - current_elements = []; - } - } else { - current_elements.push(el); - } - } - - if (current_elements.length > 0) new_args.push(b.arrayExpression(current_elements)); - - // check to see if we've just created an array of nothing but array literals, and flatten them all - // into one and get rid of the spread altogether - let all_arrays = true; - for (let a of new_args) { - if (a.type !== b.ArrayExpression) { - all_arrays = false; - break; - } - } - if (all_arrays) { - let na = []; - for (let a of new_args) na = na.concat(a.elements); - - // if we're converting an array with holes into arguments for a function, hole => undefined - na = na.map((el) => (el === null ? b.undefinedLit() : el)); - - n.arguments = na; - } else { - let receiver; - - if (n.callee.type === b.MemberExpression) receiver = n.callee.object; - else receiver = b.nullLit(); - - n.callee = b.memberExpression(n.callee, apply_id); - n.arguments = [receiver, intrinsic(arrayFromSpread_id, new_args)]; - } - - return n; - } -} diff --git a/lib/passes/desugar-spread.ts b/lib/passes/desugar-spread.ts new file mode 100644 index 00000000..2210f75f --- /dev/null +++ b/lib/passes/desugar-spread.ts @@ -0,0 +1,139 @@ +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: + */ + +// +// desugars +// +// [1, 2, ...foo, 3, 4] +// +// o.foo(1, 2, ...foo, 3, 4) +// +// to: +// +// %arrayFromSpread([1, 2], foo, [3, 4]) +// +// o.foo.apply(o, %arrayFromSpread([1, 2], foo, [3, 4]) +// + +import { TransformPass, VisitResult } from "../node-visitor"; +import * as b from "../ast-builder"; +import { intrinsic, is_intrinsic } from "../echo-util"; +import { + arrayFromSpread_id, + apply_id, + constructSuperApply_id, + constructApply_id, +} from "../common-ids"; +import type * as e from "../estree"; + +// split `args` into %arrayFromSpread operands: runs of plain arguments +// become array literals, spread arguments pass through as iterables +function spreadChunks(args: (e.Expression | e.SpreadElement | null)[]): e.Expression[] { + const chunks: e.Expression[] = []; + let current: (e.Expression | e.SpreadElement | null)[] = []; + for (const el of args) { + if (el && el.type === "SpreadElement") { + if (current.length > 0) { + chunks.push(b.arrayExpression(current)); + current = []; + } + chunks.push(el.argument); + } else { + current.push(el); + } + } + if (current.length > 0) chunks.push(b.arrayExpression(current)); + return chunks; +} + +// holes become undefined when array elements turn into call arguments +function holeToUndefined(el: e.Expression | e.SpreadElement | null): e.Expression | e.SpreadElement { + return el === null ? b.undefinedLit() : el; +} + +export class DesugarSpread extends TransformPass { + override visitArrayExpression(n: e.ArrayExpression): VisitResult { + super.visitArrayExpression(n); + const needs_desugaring = n.elements.some((el) => el && el.type === "SpreadElement"); + if (!needs_desugaring) return n; + + const chunks = spreadChunks(n.elements); + if (chunks.every((a) => a.type === "ArrayExpression")) { + // spreads of array literals only: flatten back into one literal + let flat: (e.Expression | e.SpreadElement | null)[] = []; + for (const a of chunks) flat = flat.concat((a as e.ArrayExpression).elements); + n.elements = flat; + return n; + } + return intrinsic(arrayFromSpread_id, chunks); + } + + // new Foo(...args) -> %constructApply(Foo, %arrayFromSpread(...)); + // constructs through the runtime's dense-array apply + override visitNewExpression(n: e.NewExpression): VisitResult { + super.visitNewExpression(n); + if (!n.arguments.some((el) => el.type === "SpreadElement")) return n; + const chunks = spreadChunks(n.arguments); + if (chunks.every((a) => a.type === "ArrayExpression")) { + let flat: (e.Expression | e.SpreadElement)[] = []; + for (const a of chunks) + flat = flat.concat((a as e.ArrayExpression).elements.map(holeToUndefined)); + n.arguments = flat; + return n; + } + return intrinsic(constructApply_id, [n.callee, intrinsic(arrayFromSpread_id, chunks)]); + } + + override visitCallExpression(n: e.CallExpression): VisitResult { + super.visitCallExpression(n); + + // super(...args) / super.foo(...args) can't be rewritten to an + // .apply call. this pass runs before DesugarClasses; leave super + // calls alone — DesugarClasses rewrites them into ordinary calls, + // and the post-classes run of this pass desugars what remains. + if (n.callee.type === "Super") return n; + if (n.callee.type === "MemberExpression" && n.callee.object.type === "Super") return n; + + const needs_desugaring = n.arguments.some((el) => el.type === "SpreadElement"); + if (!needs_desugaring) return n; + + // super(...args), already desugared by DesugarClasses (which runs + // first) into %constructSuper(ref, ...args): the intrinsic isn't a + // value and can't be .apply'd — use the runtime's apply form. + if (is_intrinsic(n, "%constructSuper")) { + const super_ref = n.arguments[0] as e.Expression; + const chunks = spreadChunks(n.arguments.slice(1)); + if (chunks.every((a) => a.type === "ArrayExpression")) { + // spreads of array literals only: flatten back to a plain + // %constructSuper (holes become undefined, as below) + let flat: (e.Expression | e.SpreadElement)[] = []; + for (const a of chunks) + flat = flat.concat((a as e.ArrayExpression).elements.map(holeToUndefined)); + n.arguments = [super_ref, ...flat]; + } else { + n.callee = constructSuperApply_id; + n.arguments = [super_ref, intrinsic(arrayFromSpread_id, chunks)]; + } + return n; + } + + const chunks = spreadChunks(n.arguments); + if (chunks.every((a) => a.type === "ArrayExpression")) { + // if we're converting an array with holes into arguments for a + // function, hole => undefined + let flat: (e.Expression | e.SpreadElement)[] = []; + for (const a of chunks) + flat = flat.concat((a as e.ArrayExpression).elements.map(holeToUndefined)); + n.arguments = flat; + return n; + } + + const receiver: e.Expression = + n.callee.type === "MemberExpression" ? (n.callee.object as e.Expression) : b.nullLit(); + + n.callee = b.memberExpression(n.callee, apply_id); + n.arguments = [receiver, intrinsic(arrayFromSpread_id, chunks)]; + return n; + } +} diff --git a/lib/passes/desugar-templates.js b/lib/passes/desugar-templates.js deleted file mode 100644 index a94ced08..00000000 --- a/lib/passes/desugar-templates.js +++ /dev/null @@ -1,83 +0,0 @@ -/* -*- Mode: js2; indent-tabs-mode: nil; tab-width: 4; js2-indent-offset: 4; js2-basic-offset: 4; -*- - * vim: set ts=4 sw=4 et tw=99 ft=js: - */ - -// for template strings without a tag (i.e. of the form -// `literal ${with}${possibly}${substitutions}`) we simply -// inline the spec'ed behavior of the default handler (zipping -// together the cooked values and substitutions to form the -// result.) -// -// for tagged templates: (i.e. of the form tag`literal`) we -// create a function which lazily generates the const/frozen -// callsite_id, which is a unique object containing both raw -// and cooked literal portions of the template literal. -// -// This function is invoked to get the callsiteId, which is -// then passed (along with the array of substitutions) to the -// handler named by "tag" above. -// - -import { startGenerator, intrinsic } from "../echo-util"; -import * as b from "../ast-builder"; -import { templateCallsite_id, templateDefaultHandlerCall_id } from "../common-ids"; -import { TransformPass } from "../node-visitor"; - -let callsiteGen = startGenerator(); -let freshCallsiteId = () => `%callsiteId_${callsiteGen()}`; - -export class DesugarTemplates extends TransformPass { - visitBlock(n, callsites) { - callsites = []; - n = super.visitBlock(n, callsites); - // prepend the callsite generation functions (generated by desugaring tagged template expressions below) - n.body = callsites.concat(n.body); - return n; - } - - generateCreateCallsiteIdFunc(name, quasis, loc) { - let raw_elements = []; - let cooked_elements = []; - for (let q of quasis) { - raw_elements.push(b.literal(q.value.raw)); - cooked_elements.push(b.literal(q.value.cooked)); - } - - let raw = b.arrayExpression(raw_elements); - let cooked = b.arrayExpression(cooked_elements); - - return b.functionDeclaration( - b.identifier(`generate_${name}`), - [], - b.blockStatement( - [ - b.expressionStatement( - intrinsic(templateCallsite_id, [b.literal(name), raw, cooked]) - ), - ], - loc - ), - [], - null, - loc - ); - } - - visitTaggedTemplateExpression(n, callsites) { - let callsiteid_func_id = freshCallsiteId(); - let callsite_func = this.generateCreateCallsiteIdFunc( - callsiteid_func_id, - n.quasi.quasis, - n.loc - ); - callsites.push(callsite_func); - let callsiteid_func_call = b.callExpression(callsite_func.id, []); - return b.callExpression(n.tag, [callsiteid_func_call].concat(n.quasi.expressions)); - } - - visitTemplateLiteral(n) { - let cooked = b.arrayExpression(n.quasis.map((q) => b.literal(q.value.cooked))); - let substitutions = b.arrayExpression(n.expressions); - return intrinsic(templateDefaultHandlerCall_id, [cooked, substitutions]); - } -} diff --git a/lib/passes/desugar-update-assignments.js b/lib/passes/desugar-update-assignments.js deleted file mode 100644 index 68a2572b..00000000 --- a/lib/passes/desugar-update-assignments.js +++ /dev/null @@ -1,103 +0,0 @@ -/* -*- Mode: js2; indent-tabs-mode: nil; tab-width: 4; js2-indent-offset: 4; js2-basic-offset: 4; -*- - * vim: set ts=4 sw=4 et tw=99 ft=js: - */ -// we split up assignment operators +=/-=/etc into their -// component operator + assignment so we can mark lhs as -// setLocal/setGlobal/etc, and rhs getLocal/getGlobal/etc - -import { TransformPass } from "../node-visitor"; -import { startGenerator } from "../echo-util"; -import * as b from "../ast-builder"; -import { reportError } from "../errors"; - -let updateGen = startGenerator(); -let freshUpdate = () => `%update_${updateGen()}`; - -export class DesugarUpdateAssignments extends TransformPass { - constructor(options) { - super(options); - this.debug = true; - this.updateGen = startGenerator(); - } - - visitProgram(n) { - n.prepends = []; - n = super.visitProgram(n, n); - if (n.prepends.length > 0) n.body = n.prepends.concat(n.body); - return n; - } - - visitBlock(n) { - n.prepends = []; - n = super.visitBlock(n, n); - if (n.prepends.length > 0) n.body = n.prepends.concat(n.body); - return n; - } - - visitAssignmentExpression(n, parentBlock) { - n = super.visitAssignmentExpression(n, parentBlock); - - // we only care about the $= operators, where $ = *,/,-,+,%,etc - if (n.operator.length === 1) return n; - - if (n.left.type === b.Identifier) { - // for identifiers we just expand a += b to a = a + b - n.right = b.binaryExpression(n.left, n.operator[0], n.right); - n.operator = "="; - return n; - } - - if (n.left.type === b.MemberExpression) { - let complex_exp = (n) => { - if (!n) return false; - if (n.type === b.Literal) return false; - if (n.type === b.Identifier) return false; - return true; - }; - - let prepend_update = () => { - let update_id = b.identifier(freshUpdate()); - parentBlock.prepends.unshift(b.letDeclaration(update_id, b.undefinedLit())); - return update_id; - }; - - let object_exp = n.left.object; - let prop_exp = n.left.property; - - let expressions = []; - - if (complex_exp(object_exp)) { - let update_id = prepend_update(); - expressions.push(b.assignmentExpression(update_id, "=", object_exp)); - n.left.object = update_id; - } - - if (complex_exp(prop_exp)) { - let update_id = prepend_update(); - expressions.push(b.assignmentExpression(update_id, "=", prop_exp)); - n.left.property = update_id; - } - - n.right = b.binaryExpression( - b.memberExpression(n.left.object, n.left.property, n.left.computed), - n.operator[0], - n.right - ); - n.operator = "="; - - if (expressions.length !== 0) { - expressions.push(n); - return b.sequenceExpression(expressions); - } - - return n; - } - - reportError( - Error, - `unexpected expression type ${n.left.type} in update assign expression.`, - this.filename, - n.left.loc - ); - } -} diff --git a/lib/passes/eq-idioms.js b/lib/passes/eq-idioms.js deleted file mode 100644 index 9f9af38c..00000000 --- a/lib/passes/eq-idioms.js +++ /dev/null @@ -1,122 +0,0 @@ -/* -*- Mode: js2; indent-tabs-mode: nil; tab-width: 4; js2-indent-offset: 4; js2-basic-offset: 4; -*- - * vim: set ts=4 sw=4 et tw=99 ft=js: - */ -// -// EqIdioms checks for the following things: -// -// typeof() checks against constant strings. -// -// For most cases we can inline the test directly into LLVM IR (in -// compiler.coffee), and in the cases where we can't easily, we can -// call specialized runtime builtins that don't require us to -// allocate a string do a comparison. -// -// ==/!= of constants null or undefined -// - -import { - typeofIsObject_id, - typeofIsFunction_id, - typeofIsString_id, - typeofIsSymbol_id, - typeofIsNumber_id, - typeofIsBoolean_id, - isNull_id, - isUndefined_id, - isNullOrUndefined_id, -} from "../common-ids"; -import * as b from "../ast-builder"; -import { is_intrinsic, create_intrinsic } from "../echo-util"; -import { TreeVisitor } from "../node-visitor"; - -function is_typeof(e) { - return e.type === b.UnaryExpression && e.operator === "typeof"; -} -function is_string_literal(e) { - return e.type === b.Literal && typeof e.value === "string"; -} -function is_undefined_literal(e) { - return e.type === b.Literal && e.value === undefined; -} -function is_null_literal(e) { - return e.type === b.Literal && e.value === null; -} -function is_null_or_undefined_literal(e) { - return is_undefined_literal(e) || is_null_literal(e); -} - -function eq_neq_op(op) { - return op === "==" || op === "!=" || op === "===" || op === "!=="; -} - -function op_coerces(op) { - return op.length == 2; -} - -function maybe_not(op, exp) { - if (op[0] === "!") { - return b.unaryExpression("!", exp); - } - return exp; -} - -const typecheckIntrinsics = { - object: typeofIsObject_id, - function: typeofIsFunction_id, - string: typeofIsString_id, - symbol: typeofIsSymbol_id, - number: typeofIsNumber_id, - boolean: typeofIsBoolean_id, - null: isNull_id, - undefined: isUndefined_id, -}; - -export class EqIdioms extends TreeVisitor { - visitBinaryExpression(exp) { - if (!eq_neq_op(exp.operator)) { - return super.visitBinaryExpression(exp); - } - - let left = exp.left; - let right = exp.right; - - // for typeof checks against string literals, both == && === work - if ( - (is_typeof(left) && is_string_literal(right)) || - (is_typeof(right) && is_string_literal(left)) - ) { - let typecheck = is_typeof(left) ? right.value : left.value; - let typeofarg = is_typeof(left) ? left.argument : right.argument; - - let intrinsic = typecheckIntrisics[typecheck]; - if (!intrinsic) { - throw new Error(`invalid typeof check against '${typecheck}'`); - } - - return maybe_not(exp.operator, create_intrinsic(intrinsic, [typeofarg])); - } - - // check for null/undefined comparisons - if (!is_null_or_undefined_literal(left) && !is_null_or_undefined_literal(right)) { - return super.visitBinaryExpression(exp); - } - - // one or both of subexpressions are null/undefined literals - - if (op_coerces(exp.operator)) { - // == or != here, so we need to match both (hence isNullOrUndefined). - let checkarg = is_null_or_undefined_literal(left) ? right : left; - return maybe_not(exp.operator, create_intrinsic(isNullOrUndefined_id, [checkarg])); - } - - // === or !== below here. at least one of left/right is either null or undefined literal - if (is_null_literal(left) || is_null_literal(right)) { - let checkarg = is_null_literal(left) ? right : left; - return maybe_not(exp.operator, create_intrinsic(isNull_id, [checkarg])); - } - - // === or !== below here. at least one of left/right is undefined literal - let checkarg = is_undefined_literal(left) ? right : left; - return maybe_not(exp.operator, create_intrinsic(isUndefined_id, [checkarg])); - } -} diff --git a/lib/passes/func-decls-to-vars.js b/lib/passes/func-decls-to-vars.js deleted file mode 100644 index 20051146..00000000 --- a/lib/passes/func-decls-to-vars.js +++ /dev/null @@ -1,29 +0,0 @@ -/* -*- Mode: js2; indent-tabs-mode: nil; tab-width: 4; js2-indent-offset: 4; js2-basic-offset: 4; -*- - * vim: set ts=4 sw=4 et tw=99 ft=js: - */ - -// convert all function declarations to variable assignments -// with named function expressions. -// -// i.e. from: -// function foo() { } -// to: -// var foo = function foo() { } -// - -import * as b from "../ast-builder"; -import { TransformPass } from "../node-visitor"; - -export class FuncDeclsToVars extends TransformPass { - visitFunctionDeclaration(n) { - if (n.toplevel) { - n.body = this.visit(n.body); - return n; - } else { - let func_exp = n; - func_exp.type = b.FunctionExpression; - func_exp.body = this.visit(func_exp.body); - return b.varDeclaration(b.identifier(n.id.name), func_exp); - } - } -} diff --git a/lib/passes/gather-imports.js b/lib/passes/gather-imports.js deleted file mode 100644 index d54db8e7..00000000 --- a/lib/passes/gather-imports.js +++ /dev/null @@ -1,372 +0,0 @@ -/* -*- Mode: js2; indent-tabs-mode: nil; tab-width: 4; js2-indent-offset: 4; js2-basic-offset: 4; -*- - * vim: set ts=4 sw=4 et tw=99 ft=js: - */ - -// this class does two things -// -// 1. rewrites all sources to be relative to this.toplevel_path. i.e. if -// the following directory structure exists: -// -// externals/ -// ext1.js -// root/ -// main.js (contains: import { foo } from "modules/foo" ) -// modules/ -// foo1.js (contains: module ext1 from "../../externals/ext1") -// -// $PWD = root/ -// -// $ ejs main.js -// -// ejs will rewrite module paths such that main.js is unchanged, and -// foo1.js's module declaration reads: -// -// "../externals/ext1" -// -// 2. builds up a list (this.importList) containing the list of all -// imported modules -// - -import { reportError } from "../errors"; -import * as path from "@node-compat/path"; -import * as fs from "@node-compat/fs"; -import { TreeVisitor } from "../node-visitor"; -import { startGenerator, is_intrinsic, is_string_literal, underline } from "../echo-util"; -import { JSModuleInfo, NativeModuleInfo } from "../module-info"; -import * as b from "../ast-builder"; -import * as esprima from "../../external-deps/esprima/esprima-es6"; - -let hasOwn = Object.prototype.hasOwnProperty; - -function isNativeModule(source) { - return source[0] === "@"; -} - -let allModules = new Map(); -let nativeModules = new Map(); - -class GatherImports extends TreeVisitor { - constructor(filename, p, toplevel_path, import_vars) { - super(); - this.filename = filename; - this.path = p; - this.toplevel_path = toplevel_path; - this.import_vars = import_vars; - - this.importList = []; - // remove our .js suffix since all imports are suffix-free - if (path.extname(this.filename) === ".js") { - this.filename = this.filename.substring(0, this.filename.length - 3); - } - - this.moduleInfo = new JSModuleInfo(this.filename); - allModules.set(this.filename, this.moduleInfo); - } - - addSource(n) { - if (!n.source) return n; - - if (!is_string_literal(n.source)) throw new Error("import sources must be strings"); - - let source_path = n.source.value; - - for (let v of this.import_vars) { - source_path = source_path.replace(`$${v.variable}`, v.value); - } - - if (isNativeModule(source_path)) { - if (this.importList.indexOf(source_path) === -1) this.importList.push(source_path); - this.moduleInfo.addImportSource(source_path); - - n.source_path = b.literal(source_path); - return n; - } - - if (source_path[0] !== "/") - source_path = path.resolve(this.toplevel_path, this.path, source_path); - - if (source_path.indexOf(process.cwd()) === 0) - source_path = path.relative(process.cwd(), source_path); - - if (this.importList.indexOf(source_path) === -1) this.importList.push(source_path); - this.moduleInfo.addImportSource(source_path); - - n.source_path = b.literal(source_path); - return n; - } - - addDefaultExport(path) { - this.moduleInfo.addExport("default"); - this.moduleInfo.setHasDefaultExport(); - } - - addExportIdentifier(id, constval) { - if (id === "default") this.moduleInfo.setHasDefaultExport(); - this.moduleInfo.addExport(id, constval); - } - - visitImportDeclaration(n) { - return this.addSource(n); - } - - visitExportDefaultDeclaration(n) { - // XXX more here? - this.addDefaultExport(); - } - - visitExportNamedDeclaration(n) { - if (n.declaration && (n.specifiers.length > 0 || n.source)) { - reportError(Error, "invalid state in ExportNamedDeclaration", this.filename, n.loc); - } - - n = this.addSource(n); - - if (n.specifiers.length > 0) { - for (let spec of n.specifiers) { - this.addExportIdentifier(spec.exported.name); - } - } else if (n.declaration) { - let declaration = n.declaration; - if (Array.isArray(declaration)) { - for (let decl of declaration) { - this.addExportIdentifier(decl.id.name); - } - } else if (declaration.type === b.FunctionDeclaration) { - this.addExportIdentifier(declaration.id.name); - } else if (declaration.type === b.ClassDeclaration) { - this.addExportIdentifier(declaration.id.name); - } else if (declaration.type === b.VariableDeclaration) { - for (let decl of declaration.declarations) { - this.addExportIdentifier( - decl.id.name, - declaration.kind === "const" && decl.init.type === b.Literal - ? decl.init - : undefined - ); - } - } else if (declaration.type === b.VariableDeclarator) { - this.addExportIdentifier(declaration.id.name); - } else { - throw new Error("unhandled case in visitExportNamedDeclaration"); - } - } else { - throw new Error("unhandled case in visitExportNamedDeclaration"); - } - } - - visitExportAllDeclaration(n) { - throw new Error("GatherImports#visitExportAllDeclaration unimplemented"); - } - - visitModuleDeclaration(n) { - return this.addSource(n); - } -} - -export function getAllModules() { - return allModules; -} - -function dumpModule(m) { - console.log(`'${m.path}'`); - console.log(` has default: ${m.hasDefaultExport()}`); - if (m.exports.size > 0) { - console.log(" slots:"); - m.exports.forEach((v, k) => { - console.log(` ${k}: ${v.slot_num}`); - }); - } -} - -export function dumpModules() { - console.log(underline("modules")); - allModules.forEach((m) => dumpModule(m)); -} - -function gatherImports(filename, path, top_path, tree, import_vars) { - let visitor = new GatherImports(filename, path, top_path, import_vars); - visitor.visit(tree); - return visitor.importList; -} - -function parseFile(filename, content, options) { - try { - if (!options.quiet) { - // loop over import variables, replacing their values with - // their names for output - let output_name = filename; - for (let ivar of options.import_variables) { - output_name = output_name.replace(ivar.value, `$${ivar.variable}`); - } - options.stdout_writer.write(`PARSE ${output_name}`); - } - return esprima.parse(content, { loc: true, raw: true, tolerant: true }); - } catch (e) { - console.warn(`${filename}: ${e}:`); - process.exit(-1); - } -} - -function getModuleFile(module_info, platform) { - if (typeof module_info.module_file == "string") { - return module_info.module_file; - } - if (!module_info.module_file[platform]) { - throw new Error( - `module ${module_info.module_name} doesn't have a module file for platform ${platform}` - ); - } - return module_info.module_file[platform]; -} - -function getModuleLinkFlags(module_info, platform) { - if (typeof module_info.link_flags == "string") { - return module_info.link_flags; - } - if (!module_info.link_flags[platform]) { - throw new Error( - `module ${module_info.module_name} doesn't have a link flags for platform ${platform}` - ); - } - return module_info.link_flags[platform]; -} - -function registerNativeModuleInfo( - ejs_dir, - module_name, - link_flags, - module_files, - module_info, - platform -) { - if (module_info.link_flags) - link_flags = link_flags.concat(getModuleLinkFlags(module_info, platform)); - if (module_info.module_file) - module_files = module_files.concat(getModuleFile(module_info, platform)); - - if (module_info.init_function) { - // this module can be imported - let m = new NativeModuleInfo( - module_name, - module_info.init_function, - link_flags, - module_files, - ejs_dir - ); - if (module_info.exports) module_info.exports.forEach((v) => m.addExport(v)); - - nativeModules.set(module_name, m); - } - if (module_info.submodules) { - for (let sm of module_info.submodules) { - if (!sm.module_name) - throw new Error(`${module_name} submodule missing module_name property`); - registerNativeModuleInfo( - ejs_dir, - `${module_name}/${sm.module_name}`, - link_flags, - module_files, - sm - ); - } - } -} - -function gatherNativeModuleInfo(ejs_file, platform) { - let module_info = JSON.parse(fs.readFileSync(ejs_file, "utf-8")); - let module_name = module_info.module_name || path.basename(ejs_file, ".ejs"); - - registerNativeModuleInfo(path.dirname(ejs_file), module_name, [], [], module_info, platform); -} - -function gatherAllNativeModules(module_dirs, platform) { - // gather a list of all native modules, flattening their submodule lists - for (let mdir of module_dirs) { - try { - let files = fs.readdirSync(mdir); - files.forEach((f) => { - if (path.extname(f) === ".ejs") { - try { - gatherNativeModuleInfo(path.resolve(mdir, f), platform); - } catch (e) { - console.warn(`parsing of module file ${f} failed: ${e}`); - } - } - }); - } catch (e) {} - } -} - -export function gatherAllModules(file_args, options, triple) { - let work_list = file_args.slice(); - let files = []; - - gatherAllNativeModules( - options.native_module_dirs, - `${triple.os}-${triple.arch}` - ); - - // starting at the main file, gather all files we'll need - while (work_list.length !== 0) { - let file = work_list.pop(); - - let found = false; - let jsfile = file; - if (path.extname(jsfile) !== ".js") { - jsfile = jsfile + ".js"; - } - - try { - found = fs.statSync(jsfile).isFile(); - } catch (e) { - found = false; - } - - if (!found) { - try { - if (fs.statSync(file).isDirectory()) { - jsfile = path.join(file, "index.js"); - found = fs.statSync(jsfile).isFile(); - } - } catch (e) { - found = false; - } - } - - if (found) { - let file_contents = fs.readFileSync(jsfile, "utf-8"); - let file_ast = parseFile(jsfile, file_contents, options); - - let imports = gatherImports( - file, - path.dirname(jsfile), - process.cwd(), - file_ast, - options.import_variables - ); - - files.push({ file_name: file, file_ast: file_ast }); - - for (let i of imports) { - if (work_list.indexOf(i) === -1 && !files.some((el) => el.file_name === i)) { - work_list.push(i); - } - } - } else { - // check if the file is a native module - if (!allModules.has(file)) { - if (file[0] != "@") { - throw new Error(`module ${file} not found`); - } - let native_path = file.slice(1); - if (!nativeModules.has(native_path)) { - throw new Error(`native module ${file} not found`); - } - - allModules.set(file, nativeModules.get(native_path)); - } - } - } - - return files; -} diff --git a/lib/passes/gather-imports.ts b/lib/passes/gather-imports.ts new file mode 100644 index 00000000..d927b1f9 --- /dev/null +++ b/lib/passes/gather-imports.ts @@ -0,0 +1,470 @@ +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: + */ + +// this pass does two things +// +// 1. rewrites all sources to be relative to the toplevel path, recording +// the resolved path on each import/export node as `source_path`; +// +// 2. builds up the module graph: a ModuleInfo per JS module (exports, +// slots, import list) plus the native-module registry parsed from +// .ejs manifests. + +import { reportError } from "../errors"; +import * as path from "@node-compat/path"; +import * as fs from "@node-compat/fs"; +import { TreeVisitor, VisitResult } from "../node-visitor"; +import { is_string_literal, underline } from "../echo-util"; +import { JSModuleInfo, NativeModuleInfo, ModuleInfo } from "../module-info"; +import * as b from "../ast-builder"; +import * as esprima from "../../external-deps/esprima/esprima-es6"; +import type * as e from "../estree"; +import type { CompilerOptions, ImportVariable } from "../options"; +import { passes } from "../pass-config"; +import type { Triple } from "../triple"; + +function isNativeModule(source: string): boolean { + return source[0] === "@"; +} + +const allModules = new Map(); +const nativeModules = new Map(); + +type SourcedNode = e.ImportDeclaration | e.ExportNamedDeclaration | e.ExportAllDeclaration; + +class GatherImports extends TreeVisitor { + filename: string; + path: string; + toplevel_path: string; + import_vars: ImportVariable[]; + importList: string[] = []; + moduleInfo: JSModuleInfo; + + constructor(filename: string, p: string, toplevel_path: string, import_vars: ImportVariable[]) { + super(); + this.filename = filename; + this.path = p; + this.toplevel_path = toplevel_path; + this.import_vars = import_vars; + + // remove our .js suffix since all imports are suffix-free + if (path.extname(this.filename) === ".js") { + this.filename = this.filename.substring(0, this.filename.length - 3); + } + + this.moduleInfo = new JSModuleInfo(this.filename); + allModules.set(this.filename, this.moduleInfo); + } + + private addSource(n: T): T { + if (!n.source) return n; + + if (!is_string_literal(n.source)) throw new Error("import sources must be strings"); + + let source_path = String(n.source.value); + + for (const v of this.import_vars) { + source_path = source_path.replace(`$${v.variable}`, v.value); + } + + if (!isNativeModule(source_path)) { + if (source_path[0] !== "/") + source_path = path.resolve(this.toplevel_path, this.path, source_path); + + if (source_path.indexOf(process.cwd()) === 0) + source_path = path.relative(process.cwd(), source_path); + } + + if (this.importList.indexOf(source_path) === -1) this.importList.push(source_path); + this.moduleInfo.addImportSource(source_path); + + n.source_path = b.literal(source_path) as e.Literal & { value: string }; + return n; + } + + private addExportIdentifier(id: string, constval?: e.Literal): void { + if (id === "default") this.moduleInfo.setHasDefaultExport(); + this.moduleInfo.addExport(id, constval); + } + + override visitImportDeclaration(n: e.ImportDeclaration): VisitResult { + return this.addSource(n); + } + + override visitExportDefaultDeclaration(n: e.ExportDefaultDeclaration): VisitResult { + this.moduleInfo.addExport("default"); + this.moduleInfo.setHasDefaultExport(); + return n; + } + + override visitExportNamedDeclaration(n: e.ExportNamedDeclaration): VisitResult { + if (n.declaration && (n.specifiers.length > 0 || n.source)) { + reportError( + Error, + "invalid state in ExportNamedDeclaration", + this.filename, + n.loc ?? undefined + ); + } + + this.addSource(n); + + if (n.specifiers.length > 0) { + for (const spec of n.specifiers) { + this.addExportIdentifier(spec.exported.name); + } + return n; + } + + const declaration = n.declaration; + if (!declaration) throw new Error("unhandled case in visitExportNamedDeclaration"); + + if (declaration.type === "FunctionDeclaration" || declaration.type === "ClassDeclaration") { + this.addExportIdentifier(declaration.id.name); + } else if (declaration.type === "VariableDeclaration") { + for (const decl of declaration.declarations) { + if (decl.id.type !== "Identifier") continue; + this.addExportIdentifier( + decl.id.name, + declaration.kind === "const" && decl.init && decl.init.type === "Literal" + ? decl.init + : undefined + ); + } + } else { + throw new Error("unhandled case in visitExportNamedDeclaration"); + } + return n; + } + + override visitExportAllDeclaration(n: e.ExportAllDeclaration): VisitResult { + throw new Error("GatherImports#visitExportAllDeclaration unimplemented"); + } +} + +export function getAllModules(): Map { + return allModules; +} + +function dumpModule(m: ModuleInfo): void { + console.log(`'${m.path}'`); + console.log(` has default: ${m.hasDefaultExport()}`); + if (m.exports.size > 0) { + console.log(" slots:"); + m.exports.forEach((v, k) => { + console.log(` ${k}: ${v.slot_num}`); + }); + } +} + +export function dumpModules(): void { + console.log(underline("modules")); + allModules.forEach((m) => dumpModule(m)); +} + +// promote non-exported module-level vars to hidden module slots; the EIR +// pipeline routes references through module_slot_load/store, so functions +// referencing mutable module state see one shared storage. +// +// only DIRECT toplevel declarations promote. a `var` re-declaration of +// the same name nested inside a toplevel statement (`if (x) { var state +// = ... }`) shares the binding but wouldn't be rewritten, so any name +// with such a nested declaration is excluded entirely. `const name = +// ` stays a plain local: it constant-folds instead. +function promoteModuleVars(moduleInfo: ModuleInfo, tree: e.Program): void { + // debugging: -fno-promote disables promotion outright; + // -fno-promote=substr1,substr2 only for matching module paths + // (bisecting promotion-related miscompiles) + const pcfg = passes(); + if (!pcfg.promote) return; + for (const pat of pcfg.promoteExclude) { + if (moduleInfo.path.indexOf(pat) !== -1) return; + } + // names declared by `var` nested below a direct toplevel statement + // (but outside any function -- function bodies are their own scope) + const nestedVarNames = new Set(); + const walkNested = (n: unknown): void => { + if (!n || typeof n !== "object") return; + if (Array.isArray(n)) { + for (const el of n) walkNested(el); + return; + } + const node = n as e.Node; + if ( + node.type === "FunctionDeclaration" || + node.type === "FunctionExpression" || + node.type === "ArrowFunctionExpression" + ) + return; + if (node.type === "VariableDeclaration" && node.kind === "var") { + for (const d of node.declarations) { + if (d.id.type === "Identifier") nestedVarNames.add(d.id.name); + } + } + for (const k of Object.keys(node)) { + if (k === "loc") continue; + walkNested((node as unknown as Record)[k]); + } + }; + for (const stmt of tree.body) { + if (stmt.type === "VariableDeclaration") continue; // direct: handled below + walkNested(stmt); + } + + for (const stmt of tree.body) { + if (stmt.type === "VariableDeclaration") { + for (const d of stmt.declarations) { + if (d.id.type !== "Identifier") continue; + if (moduleInfo.exports.has(d.id.name)) continue; // already slotted + if (nestedVarNames.has(d.id.name)) continue; + if (stmt.kind === "const" && d.init && d.init.type === "Literal") continue; + moduleInfo.addPromotedSlot(d.id.name); + } + } else if (stmt.type === "ClassDeclaration" && stmt.id) { + // classes don't hoist, so the setSlot rewrite at the source + // position is exactly their declaration semantics + if (moduleInfo.exports.has(stmt.id.name)) continue; + if (nestedVarNames.has(stmt.id.name)) continue; + moduleInfo.addPromotedSlot(stmt.id.name); + } else if (stmt.type === "FunctionDeclaration" && stmt.id) { + // function declarations promote too: their slot holds the one + // closure, so references (calls, value uses, `new Foo()`) + // resolve identically. the declaration becomes a slot store + // at its source position, so -- exactly like exported + // functions -- hoisting across toplevel *initialization* code + // is lost. + if (moduleInfo.exports.has(stmt.id.name)) continue; + if (nestedVarNames.has(stmt.id.name)) continue; + moduleInfo.addPromotedSlot(stmt.id.name); + } + } +} + +function gatherImports( + filename: string, + p: string, + top_path: string, + tree: e.Program, + import_vars: ImportVariable[] +): string[] { + const visitor = new GatherImports(filename, p, top_path, import_vars); + visitor.visit(tree); + promoteModuleVars(visitor.moduleInfo, tree); + return visitor.importList; +} + +function parseFile(filename: string, content: string, options: CompilerOptions): e.Program { + try { + if (!options.quiet) { + // loop over import variables, replacing their values with + // their names for output + let output_name = filename; + for (const ivar of options.import_variables) { + output_name = output_name.replace(ivar.value, `$${ivar.variable}`); + } + options.stdout_writer.write(`PARSE ${output_name}`); + } + // NOT tolerant: true — tolerant mode collects parse errors into + // ast.errors and returns a partial AST, which we would then + // silently miscompile (e.g. `async m() {}` object methods + // compiled to nonsense). a program that doesn't parse must fail + // loudly here. sourceType "module" is what makes import/export + // parse at all (tolerant mode used to recover past the spurious + // script-mode error on every import) and, per spec, makes the + // parse strict. + return esprima.parse(content, { loc: true, raw: true, sourceType: "module" }); + } catch (err) { + console.warn(`${filename}: ${String(err)}:`); + return process.exit(-1); + } +} + +// the .ejs manifest shape (JSON, one per native module) +interface NativeManifest { + module_name?: string; + init_function?: string; + link_flags?: string | Record; + module_file?: string | Record; + exports?: string[]; + submodules?: NativeManifest[]; +} + +function getModuleFile(manifest: NativeManifest, triple: Triple): string { + const module_file = manifest.module_file!; + if (typeof module_file == "string") { + return module_file; + } + const module_file_key = triple.toShortString(); + const file = module_file[module_file_key]; + if (!file) { + throw new Error( + `module ${manifest.module_name} doesn't have a module file for ${module_file_key}` + ); + } + return file; +} + +function getModuleLinkFlags(manifest: NativeManifest, triple: Triple): string { + const link_flags = manifest.link_flags!; + if (typeof link_flags === "string") { + return link_flags; + } + const module_file_key = triple.toShortString(); + const flags = link_flags[module_file_key]; + if (!flags) { + throw new Error( + `module ${manifest.module_name} doesn't have link flags for ${module_file_key}` + ); + } + return flags; +} + +function registerNativeModuleInfo( + ejs_dir: string, + module_name: string, + link_flags: string[], + module_files: string[], + manifest: NativeManifest, + triple: Triple +): void { + if (manifest.link_flags) + link_flags = link_flags.concat(getModuleLinkFlags(manifest, triple)); + if (manifest.module_file) module_files = module_files.concat(getModuleFile(manifest, triple)); + + if (manifest.init_function) { + // this module can be imported + const m = new NativeModuleInfo( + module_name, + manifest.init_function, + link_flags, + module_files, + ejs_dir + ); + if (manifest.exports) manifest.exports.forEach((v) => m.addExport(v)); + + nativeModules.set(module_name, m); + } + if (manifest.submodules) { + for (const sm of manifest.submodules) { + if (!sm.module_name) + throw new Error(`${module_name} submodule missing module_name property`); + registerNativeModuleInfo( + ejs_dir, + `${module_name}/${sm.module_name}`, + link_flags, + module_files, + sm, + triple + ); + } + } +} + +function gatherNativeModuleInfo(ejs_file: string, triple: Triple): void { + const manifest = JSON.parse(fs.readFileSync(ejs_file, "utf-8")) as NativeManifest; + const module_name = manifest.module_name || path.basename(ejs_file, ".ejs"); + + registerNativeModuleInfo(path.dirname(ejs_file), module_name, [], [], manifest, triple); +} + +function gatherAllNativeModules(module_dirs: string[], triple: Triple): void { + // gather a list of all native modules, flattening their submodule lists + for (const mdir of module_dirs) { + try { + const files = fs.readdirSync(mdir); + files.forEach((f) => { + if (path.extname(f) === ".ejs") { + try { + gatherNativeModuleInfo(path.resolve(mdir, f), triple); + } catch (err) { + console.warn(`parsing of module file ${f} failed: ${String(err)}`); + } + } + }); + } catch (err) { + // a missing module dir is fine + } + } +} + +export interface GatheredFile { + file_name: string; + file_ast: e.Program; +} + +export function gatherAllModules( + file_args: string[], + options: CompilerOptions, + triple: Triple +): GatheredFile[] { + const work_list = file_args.slice(); + const files: GatheredFile[] = []; + + gatherAllNativeModules(options.native_module_dirs, triple); + + // starting at the main file, gather all files we'll need + while (work_list.length !== 0) { + const file = work_list.pop()!; + + let found = false; + let jsfile = file; + if (path.extname(jsfile) !== ".js") { + jsfile = jsfile + ".js"; + } + + try { + found = fs.statSync(jsfile).isFile(); + } catch (err) { + found = false; + } + + if (!found) { + try { + if (fs.statSync(file).isDirectory()) { + jsfile = path.join(file, "index.js"); + found = fs.statSync(jsfile).isFile(); + } + } catch (err) { + found = false; + } + } + + if (found) { + const file_contents = fs.readFileSync(jsfile, "utf-8"); + const file_ast = parseFile(jsfile, file_contents, options); + + const imports = gatherImports( + file, + path.dirname(jsfile), + process.cwd(), + file_ast, + options.import_variables + ); + + files.push({ file_name: file, file_ast: file_ast }); + + for (const i of imports) { + if (work_list.indexOf(i) === -1 && !files.some((el) => el.file_name === i)) { + work_list.push(i); + } + } + } else { + // check if the file is a native module + if (!allModules.has(file)) { + if (file[0] != "@") { + throw new Error(`module ${file} not found`); + } + const native_path = file.slice(1); + const native = nativeModules.get(native_path); + if (!native) { + throw new Error(`native module ${file} not found`); + } + + allModules.set(file, native); + } + } + } + + return files; +} diff --git a/lib/passes/hoist-func-decls.js b/lib/passes/hoist-func-decls.js deleted file mode 100644 index ce37bebf..00000000 --- a/lib/passes/hoist-func-decls.js +++ /dev/null @@ -1,36 +0,0 @@ -/* -*- Mode: js2; indent-tabs-mode: nil; tab-width: 4; js2-indent-offset: 4; js2-basic-offset: 4; -*- - * vim: set ts=4 sw=4 et tw=99 ft=js: - */ -import { TransformPass } from "../node-visitor"; - -import * as b from "../ast-builder"; - -export class HoistFuncDecls extends TransformPass { - visitFunction(n) { - let decls = new Map(); - n.body = this.visit(n.body, decls); - decls.forEach((fd) => { - n.body.body.unshift(fd); - }); - return n; - } - - visitBlock(n, decls) { - if (n.body.length === 0) return n; - - let i = 0; - let e = n.body.length; - while (i < e) { - let child = n.body[i]; - if (child.type === b.FunctionDeclaration) { - decls.set(child.id.name, this.visit(child)); - n.body.splice(i, 1); - e = n.body.length; - } else { - i++; - } - } - n = super.visitBlock(n, decls); - return n; - } -} diff --git a/lib/passes/hoist-func-decls.ts b/lib/passes/hoist-func-decls.ts new file mode 100644 index 00000000..fb2d439f --- /dev/null +++ b/lib/passes/hoist-func-decls.ts @@ -0,0 +1,61 @@ +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: + */ + +// v8 semantics for function declarations: block-level declarations hoist +// to function scope, and same-name redeclarations collapse to the last +// one (the Map keying below). + +import { TransformPass, VisitResult } from "../node-visitor"; +import type * as e from "../estree"; +import type { CompilerOptions } from "../options"; + +export class HoistFuncDecls extends TransformPass { + // the current function's hoisted declarations; a stack because + // functions nest (visitFunction saves/restores around the recursion) + private decls: Map | null = null; + + // explicit, so tsc doesn't synthesize `constructor() { + // super(...arguments); }` — spreading `arguments` used to trip a + // runtime bug (the arguments object's specops ToNumber'd Symbol + // keys, so the @@iterator lookup threw; fixed in ejs-arguments.c, + // but the compiler shouldn't gratuitously depend on it either) + constructor(options: CompilerOptions) { + super(options); + } + + override visitFunction(n: e.Function): VisitResult { + const saved = this.decls; + const decls = new Map(); + this.decls = decls; + n.body = this.visitAs(n.body); + this.decls = saved; + if (n.body.type === "BlockStatement") { + const body = n.body.body; + decls.forEach((fd) => { + body.unshift(fd); + }); + } + return n; + } + + override visitBlock(n: e.BlockStatement): VisitResult { + if (n.body.length === 0) return n; + const decls = this.decls; + if (!decls) return super.visitBlock(n); + + let i = 0; + let end = n.body.length; + while (i < end) { + const child = n.body[i]!; + if (child.type === "FunctionDeclaration") { + decls.set(child.id.name, this.visitAs(child)); + n.body.splice(i, 1); + end = n.body.length; + } else { + i++; + } + } + return super.visitBlock(n); + } +} diff --git a/lib/passes/hoist-vars.js b/lib/passes/hoist-vars.js deleted file mode 100644 index 3dc1cdfe..00000000 --- a/lib/passes/hoist-vars.js +++ /dev/null @@ -1,134 +0,0 @@ -/* -*- Mode: js2; indent-tabs-mode: nil; tab-width: 4; js2-indent-offset: 4; js2-basic-offset: 4; -*- - * vim: set ts=4 sw=4 et tw=99 ft=js: - */ -// hoists all vars to the start of the enclosing function, replacing -// any initializer with an assignment expression. We also take the -// opportunity to convert the vars to lets at this point so by the time -// the LLVMIRVisitory gets to the tree there are only consts and lets -// -// i.e. from -// { -// .... -// var x = 5; -// .... -// } -// to -// { -// let x; -// .... -// x = 5; -// .... -// } -// -// we also warn if x was already hoisted (if the decl for it already exists in the toplevel scope) - -import * as b from "../ast-builder"; -import { TransformPass } from "../node-visitor"; -import { Stack } from "../stack-es6"; -import { reportWarning } from "../errors"; - -function create_empty_declarator(decl_name) { - return b.variableDeclarator(b.identifier(decl_name), b.undefinedLit()); -} - -export class HoistVars extends TransformPass { - constructor(options, filename) { - super(options); - this.filename = filename; - this.scope_stack = new Stack(); - } - - visitProgram(n) { - let vars = new Set(); - this.scope_stack.push({ func: n, vars: vars }); - n = super.visitProgram(n); - this.scope_stack.pop(); - - if (vars.size === 0) return n; - - let empty_declarators = []; - vars.forEach((varname) => empty_declarators.push(create_empty_declarator(varname))); - n.body.unshift(b.letDeclaration(empty_declarators)); - return n; - } - - visitFunction(n) { - let vars = new Set(); - this.scope_stack.push({ func: n, vars: vars }); - n = super.visitFunction(n); - this.scope_stack.pop(); - - if (vars.size === 0) return n; - - let empty_declarators = []; - vars.forEach((varname) => empty_declarators.push(create_empty_declarator(varname))); - n.body.body.unshift(b.letDeclaration(empty_declarators)); - - return n; - } - - visitFor(n) { - this.skipExpressionStatement = true; - n.init = this.visit(n.init); - this.skipExpressionStatement = false; - n.test = this.visit(n.test); - n.update = this.visit(n.update); - n.body = this.visit(n.body); - return n; - } - - visitForIn(n) { - if (n.left.type === b.VariableDeclaration) { - this.scope_stack.top.vars.add(n.left.declarations[0].id.name); - n.left = b.identifier(n.left.declarations[0].id.name); - } - n.right = this.visit(n.right); - n.body = this.visit(n.body); - return n; - } - - visitForOf(n) { - if (n.left.type === b.VariableDeclaration) { - this.scope_stack.top.vars.add(n.left.declarations[0].id.name); - n.left = b.identifier(n.left.declarations[0].id.name); - } - n.right = this.visit(n.right); - n.body = this.visit(n.body); - return n; - } - - visitVariableDeclaration(n) { - if (n.kind !== "var") return super.visitVariableDeclaration(n); // we only need to do this for var decls. - - // check to see if there are any initializers, which we'll convert to assignment expressions - let assignments = []; - n.declarations.forEach((decl) => { - if (decl.init) - assignments.push( - b.assignmentExpression(b.identifier(decl.id.name), "=", this.visit(decl.init)) - ); - }); - - // vars are hoisted to the containing function's toplevel scope - for (let decl of n.declarations) { - if (this.scope_stack.top.vars.has(decl.id.name)) - reportWarning( - `multiple var declarations for '${decl.id.name}' in this function.`, - this.filename, - n.loc - ); - this.scope_stack.top.vars.add(decl.id.name); - } - - if (assignments.length === 0) return b.emptyStatement(); - - let assign_exp; - // now return the new assignments, which will replace the original variable - // declaration node. - if (assignments.length > 1) assign_exp = b.sequenceExpression(assignments); - else assign_exp = assignments[0]; - - if (this.skipExpressionStatement) return assign_exp; - else return b.expressionStatement(assign_exp); - } -} diff --git a/lib/passes/iife-idioms.js b/lib/passes/iife-idioms.js deleted file mode 100644 index 2d57208d..00000000 --- a/lib/passes/iife-idioms.js +++ /dev/null @@ -1,180 +0,0 @@ -/* -*- Mode: js2; indent-tabs-mode: nil; tab-width: 4; js2-indent-offset: 4; js2-basic-offset: 4; -*- - * vim: set ts=4 sw=4 et tw=99 ft=js: - */ -// -// special pass to inline some common idioms dealing with IIFEs -// (immediately invoked function expressions). -// -// (function(x1, x2, ...) { ... body ... }(y1, y2, ...); -// -// This is a common way to provide scoping in ES5 and earlier. It is -// unnecessary with the addition of 'let' in ES6. We assume that all -// bindings in 'body' have been replace by 'let' or 'const' (meaning -// that all hoisting has been done.) -// -// we translate this form into the following equivalent inlined form: -// -// { -// let x1 = y1; -// let x2 = y2; -// -// ... -// -// { ... body ... } -// } -// -// we limit the inlining to those where count(y) <= count(x). -// otherwise we'd need to ensure that the evaluation of the extra y's -// takes place before the body is executed, even if they aren't used. -// -// Another form we can optimize is the following: -// -// (function() { body }).call(this) -// -// this form can be inlined directly as: -// -// { body } -// - -import * as escodegen from "../../external-deps/escodegen/escodegen-es6"; - -import * as b from "../ast-builder"; -import { Stack } from "../stack-es6"; -import { startGenerator, is_intrinsic, intrinsic } from "../echo-util"; - -import { TransformPass } from "../node-visitor"; - -import { getLocal_id } from "../common-ids"; - -export class IIFEIdioms extends TransformPass { - constructor(options) { - super(options); - this.function_stack = new Stack(); - this.iife_generator = startGenerator(); - } - - visitFunction(n) { - this.function_stack.push(n); - let rv = super.visitFunction(n); - this.function_stack.pop(); - return rv; - } - - maybeInlineIIFE(candidate, n) { - let arity = candidate.arguments[0].arguments[1].params.length; - let arg_count = candidate.arguments.length - 1; // %invokeClosure's first arg is the callee - - if (arg_count > arity) return n; - - // at this point we know we have an IIFE in an expression statement, ala: - // - // (function(x, ...) { ...body...})(y, ...); - // - // so just inline { ...body... } in place of the - // expression statement, after doing some magic to fix - // up argument bindings (done here) and return - // statements in the body (done in LLVMIRVisitor). - // - let iife_rv_id = b.identifier(`%iife_rv_${this.iife_generator()}`); - - let replacement = b.blockStatement(); - - replacement.body.push(b.letDeclaration(iife_rv_id, b.undefinedLit())); - - for (let i = 0; i < arity; i++) { - replacement.body.push( - b.letDeclaration( - candidate.arguments[0].arguments[1].params[i], - i < arg_count ? candidate.arguments[i + 1] : b.undefinedLit() - ) - ); - } - - this.function_stack.top.scratch_size = Math.max( - this.function_stack.top.scratch_size, - candidate.arguments[0].arguments[1].scratch_size - ); - - let body = candidate.arguments[0].arguments[1].body; - body.ejs_iife_rv = iife_rv_id; - body.fromIIFE = true; - - replacement.body.push(body); - - if (is_intrinsic(n.expression, "%setSlot")) { - n.expression.arguments[2] = intrinsic(getLocal_id, [iife_rv_id]); - replacement.body.push(n); - } else if ( - is_intrinsic(n.expression, "%setGlobal") || - is_intrinsic(n.expression, "%setLocal") - ) { - n.expression.arguments[1] = intrinsic(getLocal_id, [iife_rv_id]); - replacement.body.push(n); - } - - return replacement; - } - - maybeInlineIIFECall(candidate, n) { - if (candidate.arguments.length !== 2 || candidate.arguments[1].type !== b.ThisExpression) - return n; - - let iife_rv_id = b.identifier(`%iife_rv_${this.iife_generator()}`); - - let replacement = b.blockStatement(); - - replacement.body.push(b.letDeclaration(iife_rv_id, b.undefinedLit())); - - let body = candidate.arguments[0].object.arguments[1].body; - body.ejs_iife_rv = iife_rv_id; - body.fromIIFE = true; - - replacement.body.push(body); - - if (is_intrinsic(n.expression, "%setSlot")) { - n.expression.arguments[2] = intrinsic(getLocal_id, [iife_rv_id]); - replacement.body.push(n); - } else if ( - is_intrinsic(n.expression, "%setGlobal") || - is_intrinsic(n.expression, "%setLocal") - ) { - n.expression.arguments[1] = intrinsic(getLocal_id, [iife_rv_id]); - replacement.body.push(n); - } - - return replacement; - } - - visitExpressionStatement(n) { - let isMakeClosure = (a) => - is_intrinsic(a, "%makeClosure") || - is_intrinsic(a, "%makeAnonClosure") || - is_intrinsic(a, "%makeClosureNoEnv"); - - let candidate; - // bail out early if we know we aren't in the right place - if (is_intrinsic(n.expression, "%invokeClosure")) candidate = n.expression; - else if (is_intrinsic(n.expression, "%setSlot")) candidate = n.expression.arguments[2]; - else if ( - is_intrinsic(n.expression, "%setGlobal") || - is_intrinsic(n.expression, "%setLocal") - ) - candidate = n.expression.arguments[1]; - else return n; - - // at this point candidate should only be an invokeClosure intrinsic - if (!is_intrinsic(candidate, "%invokeClosure")) return n; - - if (isMakeClosure(candidate.arguments[0])) { - return this.maybeInlineIIFE(candidate, n); - } else if ( - candidate.arguments[0].type === b.MemberExpression && - isMakeClosure(candidate.arguments[0].object) && - candidate.arguments[0].property.name === "call" - ) { - return this.maybeInlineIIFECall(candidate, n); - } else { - return n; - } - } -} diff --git a/lib/passes/lambda-lift.js b/lib/passes/lambda-lift.js deleted file mode 100644 index 83d2f646..00000000 --- a/lib/passes/lambda-lift.js +++ /dev/null @@ -1,66 +0,0 @@ -/* -*- Mode: js2; indent-tabs-mode: nil; tab-width: 4; js2-indent-offset: 4; js2-basic-offset: 4; -*- - * vim: set ts=4 sw=4 et tw=99 ft=js: - */ -// -// This pass walks the tree and moves all function expressions to the toplevel. -// -// at the point where this pass runs there are a couple of assumptions: -// -// 1. there are no function declarations anywhere in the program. They have all -// been converted to 'var X = %makeClosure(%env_Y, function (%env) { ... })' -// -// 2. There are no free variables in the function expressions. -// - -import * as b from "../ast-builder"; -import { intrinsic, genGlobalFunctionName, genAnonymousFunctionName } from "../echo-util"; - -import { createArgScratchArea_id } from "../common-ids"; -import { TransformPass } from "../node-visitor"; - -export class LambdaLift extends TransformPass { - constructor(options, filename) { - super(options); - this.filename = filename; - this.functions = []; - } - - visitProgram(n) { - n = super.visitProgram(n); - n.body = this.functions.concat(n.body); - return n; - } - - maybePrependScratchArea(n) { - if (n.scratch_size > 0) - n.body.body.unshift( - b.expressionStatement( - intrinsic(createArgScratchArea_id, [b.literal(n.scratch_size)]) - ) - ); - } - - visitFunctionDeclaration(n) { - n.body = this.visit(n.body); - this.maybePrependScratchArea(n); - return n; - } - - visitFunctionExpression(n) { - let global_name; - if (n.displayName) global_name = genGlobalFunctionName(n.displayName, this.filename); - else if (n.id && n.id.name) global_name = genGlobalFunctionName(n.id.name, this.filename); - else global_name = genAnonymousFunctionName(this.filename); - - n.type = b.FunctionDeclaration; - n.id = b.identifier(global_name); - - this.functions.push(n); - - n.body = this.visit(n.body); - - this.maybePrependScratchArea(n); - - return b.identifier(global_name); - } -} diff --git a/lib/passes/name-anonymous-functions.js b/lib/passes/name-anonymous-functions.js deleted file mode 100644 index ec286f71..00000000 --- a/lib/passes/name-anonymous-functions.js +++ /dev/null @@ -1,34 +0,0 @@ -/* -*- Mode: js2; indent-tabs-mode: nil; tab-width: 4; js2-indent-offset: 4; js2-basic-offset: 4; -*- - * vim: set ts=4 sw=4 et tw=99 ft=js: - */ - -import { TransformPass } from "../node-visitor"; -import * as b from "../ast-builder"; - -import * as escodegen from "../../external-deps/escodegen/escodegen-es6"; - -export class NameAnonymousFunctions extends TransformPass { - visitAssignmentExpression(n) { - n = super.visitAssignmentExpression(n); - let lhs = n.left; - let rhs = n.right; - - // if we have the form - // = function () { } - // convert to: - // = function () { } - // if lhs.type is Identifier and rhs.type is FunctionExpression and not rhs.id?.name - // rhs.display = - // - let rhs_name = null; - if (rhs.id) rhs_name = rhs.id.name; - if (rhs.type === b.FunctionExpression && !rhs_name) - rhs.displayName = escodegen.generate(lhs); - return n; - } - - visitFunction(n) { - if (n.id && n.id.name) n.displayName = n.id.name; - return super.visitFunction(n); - } -} diff --git a/lib/passes/new-cc.js b/lib/passes/new-cc.js deleted file mode 100644 index 4d72e000..00000000 --- a/lib/passes/new-cc.js +++ /dev/null @@ -1,1153 +0,0 @@ -/* -*- Mode: js2; indent-tabs-mode: nil; tab-width: 4; js2-indent-offset: 4; js2-basic-offset: 4; -*- - * vim: set ts=4 sw=4 et tw=99 ft=js: - */ - -import * as escodegen from "../../external-deps/escodegen/escodegen-es6"; - -import * as b from "../ast-builder"; - -import * as debug from "../debug"; - -import { reportError, reportWarning } from "../errors"; - -import { createGlobalsInterface } from "../runtime"; - -let runtime_globals = createGlobalsInterface(null); - -import { Stack } from "../stack-es6"; -import { TransformPass, TreeVisitor } from "../node-visitor"; - -import { - genGlobalFunctionName, - genAnonymousFunctionName, - shallow_copy_object, - map, - foldl, - reject, - is_intrinsic, - is_string_literal, - intrinsic, - startGenerator, -} from "../echo-util"; - -let hasOwnProperty = Object.prototype.hasOwnProperty; - -import { - arrayFromSpread_id, - constructSuper_id, - constructSuperApply_id, - getGlobal_id, - getLocal_id, - invokeClosure_id, - constructClosure_id, - makeAnonClosure_id, - makeClosureEnv_id, - makeClosure_id, - makeClosureNoEnv_id, - setGlobal_id, - setLocal_id, - moduleGetSlot_id, - moduleSetSlot_id, - moduleGetExotic_id, - setSlot_id, - slot_id, -} from "../common-ids"; - -function assignStmt(l, op, r) { - return b.expressionStatement(b.assignmentExpression(l, op, r)); -} - -function slotIntrinsic(name, slot) { - return intrinsic(slot_id, [b.identifier(name), b.literal(slot)]); -} - -function setSlotIntrinsic(name, slot, value) { - try { - return intrinsic(setSlot_id, [b.identifier(name), b.literal(slot), value]); - } catch (e) { - console.log("invalid setSlot intrinsic:"); - console.log(`name = ${name}`); - console.log(`slot = ${slot}`); - console.log(`value = ${JSON.stringify(value)}`); - return null; - } -} - -function is_getset_intrinsic(n) { - if (!is_intrinsic(n)) return false; - if ( - n.callee.name === slot_id.name || - n.callee.name === getLocal_id.name || - n.callee.name === getGlobal_id.name - ) - return true; - if ( - n.callee.name === setSlot_id.name || - n.callee.name === setLocal_id.name || - n.callee.name === setGlobal_id.name - ) - return true; - return false; -} - -class Location { - constructor(block, func) { - this.block = block; - this.func = func; - } -} - -// figure out a better way to make a private static -let scope_id = 0; - -class Scope { - constructor(location) { - this.location = location; - this.bindings = new Map(); // the identifiers declared in this scope, mapping from string(name) -> Binding - this.referents = new Map(); // the references (rooted in other scopes) to identifiers declared in this scope, mapping from string(name) -> [Reference] - this.references = new Map(); // the references rooted in this scope, mapping from string(name) -> [Reference] - this.scope_id = scope_id; - this.parentScope = null; - this.children = []; - scope_id += 1; - } - - addBinding(binding) { - this.bindings.set(binding.name, binding); - binding.declaringScope = this; - } - getBinding(name) { - return this.bindings.get(name); - } - hasBinding(name) { - return this.bindings.has(name); - } - - addReferent(ref) { - let reflist = this.referents.get(ref.binding.name); - if (!reflist) { - reflist = []; - this.referents.set(ref.binding.name, reflist); - } - reflist.push(ref); - } - getReferents(name) { - return this.referents.get(name); - } - hasReferents(name) { - return this.referents.has(name); - } - - addReference(ref) { - this.references.set(ref.binding.name, ref); - ref.referencingScope = this; - if (ref.binding.type === "local" || ref.binding.type === "arg") - ref.binding.declaringScope.addReferent(ref); - } - getReference(name) { - return this.references.get(name); - } - hasReference(name) { - return this.references.has(name); - } - - isFunctionBodyScope() { - return this.location.block === this.location.func.body; - } - isAncestorOf(s) { - let _s = this.parentScope; - while (_s) { - if (_s === s) return true; - _s = _s.parentScope; - } - return false; - } - - differentFunction(otherscope) { - return this.location.func !== otherscope.location.func; - } - - debugString() { - let str = `scope `; - if (this.location.block.loc) str = `#{str} at line ${this.location.block.loc.start.line}`; - if (this.isFunctionBodyScope()) str = `${str} : for function ${this.location.func.id.name}`; - if (this.env) str = `${str} : environment = ${this.env.name}`; - return str; - } -} - -class Binding { - constructor(name, type, is_const) { - this.name = name; - this.type = type; - this.is_const = is_const; - } -} - -class LocalBinding extends Binding { - constructor(name, is_const) { - super(name, "local", is_const); - } -} - -class GlobalBinding extends Binding { - constructor(name, is_const) { - super(name, "global", is_const); - } -} - -class ModuleSlotBinding extends Binding { - constructor(moduleString, moduleExport, name, is_const) { - super(name, "module", is_const); - this.moduleString = moduleString; - this.moduleExport = moduleExport; - } - - getLoadIntrinsic() { - return intrinsic(moduleGetSlot_id, [this.moduleString, this.moduleExport]); - } - getStoreIntrinsic(val) { - return intrinsic(moduleSetSlot_id, [this.moduleString, this.moduleExport, val]); - } - - toString() { - return `moduleSlot(${this.moduleString.value} - ${this.moduleExport.value})`; - } -} - -class ModuleExoticBinding extends Binding { - constructor(moduleString, name, is_const) { - super(name, "module-exotic", is_const); - this.moduleString = moduleString; - } - - getLoadIntrinsic() { - return intrinsic(moduleGetExotic_id, [this.moduleString]); - } - // no store intrinsic -} - -class Reference { - constructor(binding) { - this.binding = binding; - } -} - -class Environment { - constructor(id, level) { - this.id = id; - this.level = level; - this.name = `%env_${this.id}`; - this.slot_map = new Map(); - this.parentEnv = null; - } - - hasSlots() { - return this.slot_map.size > 0; - } - hasSlot(name) { - return this.slot_map.has(name); - } - getSlot(name) { - let rv = this.slot_map.get(name); - if (rv === undefined) - throw new Error(`environment ${this.name} does not contain slot for ${name}`); - return rv; - } - - addSlot(name) { - if (this.slot_map.has(name)) return; - this.slot_map.set(name, this.slot_map.size); - } - - slotCount() { - return this.slot_map.size; - } - - addChild(env) { - if (!env) throw new Error("invalid null child"); - if (env.parentEnv && env.parentEnv !== this) - throw new Error( - `attempting to set parent of ${env.name} to ${this.name}, but it already has a a parent, ${env.parentEnv.name}` - ); - env.addSlot(this.name); - env.parentEnv = this; - } - - toString() { - return this.name; - } -} - -let allFunctions = []; - -let global_bindings = new Map(); - -class SubstituteVariables extends TransformPass { - constructor(options, filename, allModules) { - super(options, filename); - this.allModules = allModules; - this.filename = filename; - this.options = options; - this.current_scope = null; - } - - visitBlock(n) { - this.current_scope = n.scope; - super.visitBlock(n); - this.current_scope = this.current_scope.parentScope; - return n; - } - - env_name() { - return this.current_scope.env.name; - } - env_slot(name) { - return this.current_scope.env.getSlot(name); - } - - visitVariableDeclaration(n) { - if (n.declarations.length > 1) - throw new Error("VariableDeclarations should only have 1 declarator at this point"); - let decl = n.declarations[0]; - - decl.init = this.visit(decl.init); - // don't visit the id - - let referents = this.current_scope.getReferents(decl.id.name); - if (referents) { - for (let referent of referents) { - if (referent.referencingScope.differentFunction(this.current_scope)) { - // it's closed over, so we need to set it in our allocated environment - return b.expressionStatement( - setSlotIntrinsic( - this.env_name(), - this.env_slot(decl.id.name), - decl.init || b.undefinedLit() - ) - ); - } - } - } - return n; - } - - visitCallExpression(n) { - // if it's one of our get/set Slot/Local/Global intrinsics, bail - if (is_getset_intrinsic(n)) return n; - - // otherwise we need to visit the args - if (is_intrinsic(n)) { - n.arguments = this.visit(n.arguments); - if (is_intrinsic(n, constructSuperApply_id.name)) - this.current_scope.location.func.scratch_size = Math.max( - this.current_scope.location.func.scratch_size, - n.arguments.length + 1 - ); - else if (is_intrinsic(n, constructSuper_id.name)) - this.current_scope.location.func.scratch_size = Math.max( - this.current_scope.location.func.scratch_size, - n.arguments.length + 1 - ); - else if (is_intrinsic(n, arrayFromSpread_id.name)) - this.current_scope.location.func.scratch_size = Math.max( - this.current_scope.location.func.scratch_size, - n.arguments.length + 1 - ); - return n; - } - - n = super.visitCallExpression(n); - this.current_scope.location.func.scratch_size = Math.max( - this.current_scope.location.func.scratch_size, - n.arguments.length + 1 - ); - let rv = intrinsic(invokeClosure_id, [n.callee].concat(n.arguments)); - rv.loc = n.loc; - return rv; - } - - visitNewExpression(n) { - n = super.visitNewExpression(n); - - this.current_scope.location.func.scratch_size = Math.max( - this.current_scope.location.func.scratch_size, - n.arguments.length + 1 - ); - - let rv = intrinsic(constructClosure_id, [n.callee].concat(n.arguments)); - rv.loc = n.loc; - return rv; - } - - visitFunction(n) { - n.scratch_size = 0; - n.body = this.visit(n.body); - - if (n.toplevel) return n; - - if (n.type === b.FunctionDeclaration) - throw new Error("there should be no FunctionDeclarations at this point"); - - let intrinsic_args = []; - let intrinsic_id; - - if (n.id) { - intrinsic_id = makeClosure_id; - if (n.params[0].name === "%env_unused") intrinsic_id = makeClosureNoEnv_id; - else intrinsic_args.push(b.identifier(n.params[0].name, n.loc)); - - if (n.id.type === b.Identifier) intrinsic_args.push(b.literal(n.id.name)); - else intrinsic_args.push(b.literal(escodegen.generate(n.id))); - } else { - intrinsic_id = makeAnonClosure_id; - if (n.params[0].name === "%env_unused") intrinsic_args.push(b.undefinedLit()); - else intrinsic_args.push(b.identifier(n.params[0].name, n.loc)); - } - - intrinsic_args.push(n); - - return intrinsic(intrinsic_id, intrinsic_args); - } - - visitAssignmentExpression(n) { - if (n.left.type !== b.Identifier) return super.visitAssignmentExpression(n); - - let rhs = this.visit(n.right); - let leftname = n.left.name; - - let referents = this.current_scope.getReferents(leftname); - if (referents) { - for (let referent of referents) { - if (referent.referencingScope.differentFunction(this.current_scope)) { - // it's closed over, so we need to set it in our allocated environment - let rv = setSlotIntrinsic(this.env_name(), this.env_slot(leftname), rhs); - rv.loc = n.loc; - return rv; - } - } - } else if (this.current_scope.hasReference(leftname)) { - let ref = this.current_scope.getReference(leftname); - if (ref.binding.type === "local" || ref.binding.type === "arg") { - let declaringScope = ref.binding.declaringScope; - let declaringEnv = declaringScope.env; - if (declaringEnv && declaringEnv.hasSlot(leftname)) { - let rv = setSlotIntrinsic( - declaringEnv.name, - declaringEnv.getSlot(leftname), - rhs - ); - rv.loc = n.loc; - return rv; - } else { - let rv = intrinsic(setLocal_id, [n.left, rhs]); - rv.loc = n.loc; - return rv; - } - } else if (ref.binding.type === "global") { - if (leftname === "undefined") - reportError( - SyntaxError, - "reassigning 'undefined' not permitted.", - this.filename, - n.loc - ); - let rv = intrinsic(setGlobal_id, [n.left, rhs]); - rv.loc = n.loc; - return rv; - } else if (ref.binding.type === "module") { - let rv = ref.binding.getStoreIntrinsic(this.visit(rhs)); - rv.loc = n.loc; - return rv; - } else { - throw new Error(`unhandled binding type ${ref.binding.type}`); - } - } - - let rv = intrinsic(setLocal_id, [n.left, rhs]); - rv.loc = n.loc; - return rv; - } - - visitIdentifier(n) { - let referents = this.current_scope.getReferents(n.name); - if (referents) { - for (let referent of referents) { - if (referent.referencingScope.differentFunction(this.current_scope)) { - // it's closed over, so we need to set it in our allocated environment - return slotIntrinsic(this.env_name(), this.env_slot(n.name)); - } - } - } else if (this.current_scope.hasReference(n.name)) { - let ref = this.current_scope.getReference(n.name); - let binding = ref.binding; - if (binding.type === "local" || binding.type === "arg") { - let declaringScope = binding.declaringScope; - let declaringEnv = declaringScope.env; - - if (declaringEnv && declaringEnv.hasSlot(n.name)) { - let rv = slotIntrinsic(declaringEnv.name, declaringEnv.getSlot(n.name)); - rv.loc = n.loc; - return rv; - } else { - let rv = intrinsic(getLocal_id, [n]); - rv.loc = n.loc; - return rv; - } - } else if (binding.type === "global") { - let rv = intrinsic(getGlobal_id, [n]); - rv.loc = n.loc; - return rv; - } else if (binding.type === "module") { - // check if the export is const+literal. if it is, just propagate it here - let module_info = this.allModules.get(binding.moduleString.value); - let export_info = module_info.exports.get(binding.moduleExport.value); - if (export_info.constval) return export_info.constval; - return binding.getLoadIntrinsic(); - } else if (binding.type === "module-exotic") { - let rv = binding.getLoadIntrinsic(); - rv.loc = n.loc; - return rv; - } else { - throw new Error(`unhandled binding type ${binding.type}`); - } - } - let rv = intrinsic(getLocal_id, [n]); - rv.loc = n.loc; - return rv; - } - - visitMemberExpression(n) { - n = super.visitMemberExpression(n); - - if (!is_intrinsic(n.object, "%moduleGetExotic")) return n; - if (n.property.type !== b.Identifier && !is_string_literal(n.property)) return n; - - let moduleString = n.object.arguments[0]; - let moduleExport = n.property.type === b.Identifier ? n.property.name : n.property.raw; - - if (moduleString.value[0] === "@") return n; - if (!this.allModules.has(moduleString.value)) return n; - - // we have a member expression where the object is a module - // exotic and the property is either an identifier or a string - // literal, both of which we can resolve at compile time. - // - // rewrite it to use moduleGetSlot. - - let module_info = this.allModules.get(moduleString.value); - if (!module_info.exports.has(moduleExport)) - throw new Error(`${moduleString.value} doesn't export ${moduleExport}`); // XXX - let export_info = module_info.exports.get(moduleExport); - - let rv = intrinsic(moduleGetSlot_id, [moduleString, b.literal(moduleExport)]); - rv.loc = n.loc; - return rv; - } - - visitObjectExpression(n) { - for (let property of n.properties) { - if (property.computed) property.key = this.visit(property.key); - property.value = this.visit(property.value); - } - return n; - } - - visitCatchClause(n) { - // don't visit the parameter here or else we'll try to rewrite it as %get*(param-name) - n.body = this.visitBlock(n.body); - return n; - } - - visitLabeledStatement(n) { - // we need to override this method so we can skip the identifier being used as the label - n.body = this.visit(n.body); - return n; - } -} - -class FlattenDeclarations extends TransformPass { - constructor(options, filename, allModules) { - super(options, filename); - this.filename = filename; - this.allModules = allModules; - } - - visitBlock(n) { - let decl_map = new Map(); - n = super.visitBlock(n, decl_map); - let new_body = []; - for (let s of n.body) { - if (decl_map.has(s)) new_body = new_body.concat(decl_map.get(s)); - else new_body.push(s); - } - n.body = new_body; - return n; - } - - visitVariableDeclaration(n, decl_map) { - if (n.declarations.length == 1) return super.visitVariableDeclaration(n, decl_map); - - let decl_replacement = []; - for (let decl of n.declarations) - decl_replacement.push( - b.variableDeclaration( - n.kind, - decl.id, - decl.init ? this.visit(decl.init) : b.undefinedLit() - ) - ); - - decl_map.set(n, decl_replacement); - return n; - } -} - -class CollectScopeNestingInfo extends TransformPass { - constructor(options, filename, allModules) { - super(options, filename); - this.options = options; - this.filename = filename; - this.allModules = allModules; - this.block_stack = new Stack(); - this.func_stack = new Stack(); - this.current_scope = null; - this.root_scope = null; - } - - visitVariableDeclarator(n) { - // skip the id - n.init = this.visit(n.init); - return n; - } - - doWithScope(scope, fn) { - scope.parentScope = this.current_scope; - if (this.current_scope) this.current_scope.children.push(scope); - this.current_scope = scope; - fn(); - this.current_scope = scope.parentScope; - } - - doWithBlock(n, fn) { - this.block_stack.push(n); - fn(); - this.block_stack.pop(); - } - - doWithFunc(n, fn) { - this.func_stack.push(n); - fn(); - this.func_stack.pop(); - } - - createBindingsForScope(block, for_scope) { - for (let s of block.body) { - if (s.type === b.VariableDeclaration) { - // we're guaranteed to have variable declarations with a single declarator by the FlattenDeclarations pass - let d = s.declarations[0]; - if (d.init) { - if (is_intrinsic(d.init, "%moduleGetSlot")) { - for_scope.addBinding( - new ModuleSlotBinding( - d.init.arguments[0], - d.init.arguments[1], - d.id.name - ) - ); - // we inline module slot loads at all their use points, so we no longer need this decl at all - s.type = b.EmptyStatement; - } else if (is_intrinsic(d.init, "%moduleGetExotic")) { - for_scope.addBinding( - new ModuleExoticBinding(d.init.arguments[0], d.id.name) - ); - } else { - for_scope.addBinding(new LocalBinding(d.id.name, s.kind === "const")); - } - } else { - for_scope.addBinding(new LocalBinding(d.id.name, s.kind === "const")); - } - } else if (s.type === b.FunctionDeclaration && s.id) { - for_scope.addBinding(new LocalBinding(s.id.name, false, for_scope)); - } else if ( - s.type === b.ExpressionStatement && - is_intrinsic(s.expression, "%moduleSetSlot") - ) { - let args = s.expression.arguments; - for_scope.addBinding(new ModuleSlotBinding(args[0], args[1], args[1].value)); - } - } - } - - visitBlock(n, initial_bindings) { - let this_scope = new Scope(new Location(n, this.func_stack.top)); - if (this.root_scope === null) this.root_scope = this_scope; - - if (initial_bindings) for (let binding of initial_bindings) this_scope.addBinding(binding); - - // we have to gather decls before visiting our children - // so that if they refer to ids in this scope, we can - // create the proper Reference objects - this.createBindingsForScope(n, this_scope); - - this.doWithScope(this_scope, () => { - this.doWithBlock(n, () => { - super.visitBlock(n); - }); - }); - - Object.defineProperty(n, "scope", { value: this_scope }); - return n; - } - - visitFunction(n) { - //if (n.id) - // debug.log(`function ${n.id?.name} has idx of ${allFunctions.length}`); - - allFunctions.push(n); - //param_bindings = (new Binding(p.name, 'arg', false) for p in n.params) - this.doWithFunc(n, () => { - n.body = this.visitBlock(n.body); //, param_bindings - }); - return n; - } - - visitCatchClause(n) { - // visit our body with a new local binding for the catch parameter - n.body = this.visitBlock(n.body, [new LocalBinding(n.param.name, false)]); - return n; - } - - find_binding_in_scope(ident) { - let name = ident.name; - let s = this.current_scope; - while (s) { - if (s.hasBinding(name)) return new Reference(s.getBinding(name)); - s = s.parentScope; - } - - if (hasOwnProperty.call(runtime_globals, name)) - return new Reference(new GlobalBinding(name)); - - if (this.options.warn_on_undeclared) { - reportWarning(`undeclared identifier: ${ident.name}`, this.filename, ident.loc); - let binding = global_bindings.get(ident.name); - if (!binding) { - binding = new GlobalBinding(ident.name, false); - global_bindings.set(ident.name, binding); - } - return new Reference(binding); - } else { - reportError( - ReferenceError, - `undeclared identifier '${ident.name}'`, - this.filename, - ident.loc - ); - } - return null; - } - - visitIdentifier(n) { - this.current_scope.addReference(this.find_binding_in_scope(n)); - return n; - } - - visitObjectExpression(n) { - for (let property of n.properties) { - if (property.computed) property.key = this.visit(property.key); - property.value = this.visit(property.value); - } - return n; - } - - visitCallExpression(n) { - // if it's one of our get/set Slot/Local/Global intrinsics, bail - if (is_getset_intrinsic(n)) return n; - - // otherwise we need to visit the args - if (is_intrinsic(n)) { - n.arguments = this.visit(n.arguments); - return n; - } - return super.visitCallExpression(n); - } - - visitLabeledStatement(n) { - // we need to override this method so we can skip the identifier being used as the label - n.body = this.visit(n.body); - return n; - } -} - -function placeEnvironments(root_scope) { - let env_id = 0; - - // A map : function -> [environment] - let func_to_envs = new Map(); - - function get_func_envs(f) { - let func_idx = allFunctions.indexOf(f); - let func_envs = func_to_envs.get(func_idx); - if (!func_envs) { - func_envs = []; - func_to_envs.set(func_idx, func_envs); - } - return func_envs; - } - - function add_func_env(func_envs, env) { - if (func_envs[env.level]) { - if (func_envs[env.level] !== env) - throw new Error("multiple paths to an environment? shouldn't be possible."); - } else { - func_envs[env.level] = env; - } - } - - function dump_func_envs() { - func_to_envs.forEach((v, k) => { - debug.log(`function ${allFunctions[k].id.name} (${k}) requires these environments:`); - if (!v || v.length === 0) { - debug.log(" none!"); - } else { - for (var e of v) { - if (e) { - debug.log(` env: ${e.name}`); - } - } - } - }); - } - - // - // given an array of arrays of the form: - // - // [ , , e2 , ] - // [ e0 , e1 , , ] - // - // returns - // - // [ e0 , e1 , e2 , ] - // - // used for merging the func_env arrays returned from children - // - function flatten_func_envs(fe_arr) { - let rv = []; - for (let fe of fe_arr) { - if (fe) { - for (let idx = 0, ei = fe.length; idx < ei; idx++) { - let e = fe[idx]; - if (e) rv[idx] = e; // should probably check if !rv[idx] or rv[idx] === e - } - } - } - return rv; - } - - function dump_scopes(s, level) { - //debug.log(s.debugString()); - s.children.forEach((c) => { - //debug.indent(); - dump_scopes(c, level + 1); - //debug.unindent(); - }); - } - - function walk_scope1(s, level) { - // - // Collect external references to bindings defined in this scope. - // - // referent reference - // s <-------------> binding <---------------> referencingScope - // - //debug.log(`dealing with scope from line ${s.location.block.loc.start.line}, environment will be ${env_id}`); - let env = new Environment(env_id, level); - env_id++; - - // walk over this scope's referents. if any of this scope's - // bindings are referred to from outside the function, we need - // an environment - s.referents.forEach((reflist) => { - reflist.forEach((ref) => { - let referencingScope = ref.referencingScope; - //debug.log(`referent name is ${ref.binding.name}, s.func = ${s.location.func.id.name}/${s.location.func.loc.start.line}, referencing scope = ${referencingScope.location.func.id.name}/${referencingScope.location.func.loc.start.line}`); - if (referencingScope.differentFunction(s)) { - // the scopes are in different functions - //debug.log(`reference to '${ref.binding.name}' from outside declaring function (in function ${referencingScope.location.func.id.name})!`); - - // and add a slot for the referent - env.addSlot(ref.binding.name); - - // also, mark the referencing scope's function as needing this environment - let func_envs = get_func_envs(referencingScope.location.func); - add_func_env(func_envs, env); - } - }); - }); - - if (env.slotCount() > 0) { - //debug.log(`creating environment '${env.name}' for function ${s.location.func.id.name}`); - s.env = env; - } - - // recurse into our child scopes. - let child_func_envs = s.children.map((c) => { - //debug.indent(); - let ce = walk_scope1(c, level + 1); - //debug.unindent(); - return ce; - }); - - let child_env_reqs = flatten_func_envs(child_func_envs); - - // we're back in the scope passed to this function, - // having visited all parents and all children. we - // should now know exactly which parent scopes have - // environments, and should be able to calculate the - // path to any bindings we reference. - - //debug.log("before removing our environment, function ${s.location.func.id.name} has the following required (from children) environments: ${env for env in child_env_reqs}`); - - if (s.env) { - let idx = child_env_reqs.indexOf(s.env); - if (idx !== -1) { - //debug.log(`removing env ${s.env.name} from list of required environments`); - child_env_reqs.splice(idx, 1); - } - } - - //debug.log(`function ${s.location.func.id.name} has the following required (from children) environments: ${env for env in child_env_reqs}`); - - s.nestedEnvironments = child_env_reqs; - - if (s.isFunctionBodyScope()) { - let func = s.location.func; - let func_idx = allFunctions.indexOf(func); - //debug.log(" scope is function body scope for //{func_idx} //{func.id?.name}///{func.loc?.start.line}"); - - let func_envs = get_func_envs(func); - - // add the nested environments required by our descendents (that have not been added somewhere in this function) as though they are required by us - s.nestedEnvironments.forEach((nested_env) => { - if (!nested_env) return; - //debug.log "adding //{nested_env}" - add_func_env(func_envs, nested_env); - }); - - //debug.log("after adding nested environments, func_envs for function is //{fenv for fenv in func_envs}") - - return func_envs; - } - - return s.nestedEnvironments; - } - - // for every environment, calculate the parent they must have by all the paths we've computed in func_to_envs - function collapse_paths() { - let parent_envs = new Map(); - - func_to_envs.forEach((func_envs, func) => { - if (func_envs.length === 0) return; - - func_envs = func_envs.filter((a_env) => a_env); - - for (let idx = func_envs.length - 1; idx >= 1; idx--) { - let current_e = func_envs[idx]; - let prospective_parent = func_envs[idx - 1]; - if ( - !parent_envs.has(current_e) || - parent_envs.get(current_e).level < prospective_parent.level - ) { - parent_envs.set(current_e, prospective_parent); - } - } - }); - - parent_envs.forEach((e, p_e) => e.addChild(p_e)); - - // now insert dependencies for parent envs between environments that require them - func_to_envs.forEach((func_envs, func) => { - if (func_envs.length < 2) return; - - let collapsed_func_envs = func_envs.filter((a_env) => a_env); - - for (let idx = collapsed_func_envs.length - 1; idx >= 1; idx--) { - let current_e = collapsed_func_envs[idx]; - let prior_e = collapsed_func_envs[idx - 1]; - if (current_e.parentEnv !== prior_e) { - // we need to walk current_e's parent chain until we reach prior_e, adding the environments to func_envs - let e = current_e.parentEnv; - while (e !== prior_e) { - add_func_env(func_envs, e); - e = e.parentEnv; - } - } - } - }); - } - - function walk_scope2(s, level) { - //debug.log(`walk_scope2 for scope ${s.debugString()}`); - - if (s.env) { - if (s.env.parentEnv && s.env.parentEnv.slotCount() > 0) { - //debug.log "outputting parent environment assignment. parentEnv.name = //{s.env.parentEnv.name}, parentEnv.slotCount = //{s.env.parentEnv.slotCount()}" - s.location.block.body.unshift( - b.expressionStatement( - setSlotIntrinsic( - s.env.name, - s.env.getSlot(s.env.parentEnv.name), - b.identifier(s.env.parentEnv.name) - ) - ) - ); - } - s.location.block.body.unshift( - b.letDeclaration( - b.identifier(s.env.name), - intrinsic(makeClosureEnv_id, [b.literal(s.env.slot_map.size)]) - ) - ); - } else { - //debug.log "scope from line //{s.location.block.loc?.start.line} doesn't have environment" - } - - if (s.isFunctionBodyScope()) { - let func = s.location.func; - let func_idx = allFunctions.indexOf(func); - //debug.log(" ******* scope is function body scope for //{func_idx} //{func.id?.name} //{func.loc?.start.line}"); - let func_envs = get_func_envs(s.location.func); - - let env_assignments = []; - - let env_name; - - if (func_envs.length === 0) { - //debug.log "unused environment, func_envs.length = //{func_envs?.length}" - env_name = "%env_unused"; - } else { - // func_envs.length >= 1 - func_envs = func_envs.filter((a_env) => a_env); - - let last_idx = func_envs.length - 1; - - env_name = func_envs[last_idx].name; - - last_idx -= 1; - - while (last_idx >= 0) { - //debug.log "adding const //{func_envs[last_idx].name} = slotIntrinsic(//{func_envs[last_idx+1].name}, //{func_envs[last_idx+1].name}.getSlot(//{func_envs[last_idx].name}));" - //debug.log " const //{func_envs[last_idx].name} = slotIntrinsic(//{func_envs[last_idx+1].name}, //{func_envs[last_idx+1].getSlot(func_envs[last_idx].name)});" - let env_decl = b.constDeclaration( - b.identifier(func_envs[last_idx].name), - slotIntrinsic( - func_envs[last_idx + 1].name, - func_envs[last_idx + 1].getSlot(func_envs[last_idx].name) - ) - ); - env_decl.loc = func.body.loc; - env_assignments.push(env_decl); - last_idx -= 1; - } - } - - // add the parameter we need - func.params.unshift(b.identifier(env_name, func.loc)); - - // and add the assignments of all the environments this function needs - if (env_assignments.length > 0) { - func.body.body = env_assignments.concat(func.body.body); - } - } - s.children.forEach((c) => walk_scope2(c, level + 1)); - } - - walk_scope1(root_scope, 0); - collapse_paths(); - //dump_scopes(root_scope, 0); - walk_scope2(root_scope, 0); - //dump_func_envs(); -} - -function is_undefined_literal(e) { - if (e.type === b.Literal && e.value === undefined) return true; - return e.type === b.UnaryExpression && e.operator === "void" && e.argument.value === 0; -} - -class ValidateEnvironments extends TreeVisitor { - constructor(options, filename) { - super(); - this.filename = filename; - this.options = options; - } - - visitCallExpression(n) { - n = super.visitCallExpression(n); - // make sure that the environment we assign to a - // closure is the same as the first arg to the function - if (is_intrinsic(n, makeClosure_id.name) || is_intrinsic(n, makeAnonClosure_id.name)) { - //debug.log escodegen.generate n.arguments[0] - //debug.log is_undefined_literal(n.arguments[0]) - if (!is_undefined_literal(n.arguments[0])) { - let closure_env = n.arguments[0].name; - let closure_func = n.arguments[is_intrinsic(n, makeClosure_id.name) ? 2 : 1]; - let func_env = closure_func.params[0].name; - if (closure_env !== func_env) { - throw new Error( - `closure created using environment ${closure_env}, while function takes ${func_env}` - ); - } - } - } else if (is_intrinsic(n, setSlot_id.name)) { - let env_name = n.arguments[0].name; - let env_id = env_name.substring("%env_".length); - let slot = n.arguments[1].value; - } else if (is_intrinsic(n, slot_id.name)) { - let env_name = n.arguments[0].name; - let env_id = env_name.substring("%env_".length); - let slot = n.arguments[1].value; - } - return n; - } -} - -export class NewClosureConvert { - constructor(options, filename, allModules) { - this.options = options; - this.filename = filename; - this.allModules = allModules; - } - - visit(tree) { - let flattenDecls = new FlattenDeclarations(this.options, this.filename, this.allModules); - let collectScopes = new CollectScopeNestingInfo( - this.options, - this.filename, - this.allModules - ); - let substituteVariables = new SubstituteVariables( - this.options, - this.filename, - this.allModules - ); - - tree = flattenDecls.visit(tree); - - tree = collectScopes.visit(tree); - - //debug.log escodegen.generate tree - placeEnvironments(collectScopes.root_scope); - - tree = substituteVariables.visit(tree); - /* - let validator = new ValidateEnvironments(this.options, this.filename); - tree = validator.visit(tree); -*/ - - allFunctions = []; - global_bindings = new Map(); - - return tree; - } -} diff --git a/lib/passes/replace-unary-void.js b/lib/passes/replace-unary-void.js deleted file mode 100644 index 85364b0a..00000000 --- a/lib/passes/replace-unary-void.js +++ /dev/null @@ -1,15 +0,0 @@ -/* -*- Mode: js2; indent-tabs-mode: nil; tab-width: 4; js2-indent-offset: 4; js2-basic-offset: 4; -*- - * vim: set ts=4 sw=4 et tw=99 ft=js: - */ -import { TreeVisitor } from "../node-visitor"; -import * as b from "../ast-builder"; -import { builtinUndefined_id } from "../common-ids"; -import { create_intrinsic } from "../echo-util"; - -export class ReplaceUnaryVoid extends TreeVisitor { - visitUnaryExpression(n) { - if (n.operator === "void" && n.argument.type === b.Literal && n.argument.value === 0) - return create_intrinsic(builtinUndefined_id, []); - return n; - } -} diff --git a/lib/passes/substitute-variables.js b/lib/passes/substitute-variables.js deleted file mode 100644 index 3e36a9f3..00000000 --- a/lib/passes/substitute-variables.js +++ /dev/null @@ -1,468 +0,0 @@ -/* -*- Mode: js2; indent-tabs-mode: nil; tab-width: 4; js2-indent-offset: 4; js2-basic-offset: 4; -*- - * vim: set ts=4 sw=4 et tw=99 ft=js: - */ -// 1) allocates the environment at the start of the n -// 2) adds mappings for all .closed variables -import { TransformPass } from "../node-visitor"; -import { Stack } from "../stack-es6"; - -import { intrinsic, is_intrinsic, reject, shallow_copy_object } from "../echo-util"; - -import * as b from "../ast-builder"; - -import { - invokeClosure_id, - makeClosure_id, - makeAnonClosure_id, - makeClosureEnv_id, - setSlot_id, - slot_id, -} from "../common-ids"; - -import * as escodegen from "../../external-deps/escodegen/escodegen-es6"; - -let hasOwnProperty = Object.prototype.hasOwnProperty; - -export class SubstituteVariables extends TransformPass { - constructor(options) { - super(options); - this.function_stack = new Stack(); - this.mappings = new Stack(); - } - - currentMapping() { - return this.mappings.depth > 0 ? this.mappings.top : Object.create(null); - } - - visitIdentifier(n) { - if (hasOwnProperty.call(this.currentMapping(), n.name)) - return this.currentMapping()[n.name]; - return n; - } - - visitFor(n) { - // for loops complicate things. - // if any of the variables declared in n.init are closed over - // we promote all of them outside of n.init. - - this.skipExpressionStatement = true; - let init = this.visit(n.init); - this.skipExpressionStatement = false; - n.test = this.visit(n.test); - n.update = this.visit(n.update); - n.body = this.visit(n.body); - if (Array.isArray(init)) { - n.init = null; - return b.blockStatement(init.concat([n])); - } - n.init = init; - return n; - } - - visitForIn(n) { - // for-in loops complicate things. - - let left = this.visit(n.left); - n.right = this.visit(n.right); - n.body = this.visit(n.body); - if (Array.isArray(left)) { - console.log("whu?"); - n.left = b.identifier(left[0].declarations[0].id.name); - return b.blockStatement(left.concat([n])); - } - - n.left = left; - return n; - } - - visitVariableDeclaration(n) { - // here we do some magic depending on whether or not - // variables are closed over (i.e. pushed into the - // environment). - // - // 1. for variables that are closed over that aren't - // initialized (that is, they're implicitly - // 'undefined'), we just remove their declaration - // entirely. it's already been converted to a slot - // everywhere else, and env slots are explicitly - // initialized to undefined by the runtime. - // - // 2. for variables that are closed over that *are* - // initialized, we splice them into the list and - // split the VariableDeclaration node into two, so - // if 'y' is closed over in the following input: - // - // let x = 2, y = x * 2, z = 10; - // - // we'll end up with this in the output: - // - // let x = 2; - // %slot(%env, 1, 'y') = x * 2; - // let z = 10; - // - let decls = n.declarations; - - let rv = []; - - let new_declarations = []; - - // we loop until we find a variable that's closed over *and* has an initializer. - for (let decl of decls) { - decl.init = this.visit(decl.init); - - let closed_over = hasOwnProperty.call(this.currentMapping(), decl.id.name); - if (closed_over) { - // for variables that are closed over but undefined, we skip them (thereby removing them from the list of decls) - - if (decl.init) { - // FIXME: we should also check for an explicit 'undefined' here - - // push the current set of new_declarations if there are any - if (new_declarations.length > 0) - rv.push(b.variableDeclaration(n.kind, new_declarations)); - - // splice in this assignment - rv.push( - b.expressionStatement( - b.assignmentExpression( - this.currentMapping()[decl.id.name], - "=", - decl.init - ) - ) - ); - - // then re-init the new_declarations array - new_declarations = []; - } - } else { - // for variables that aren't closed over, we just add them to the currect decl list. - new_declarations.push(decl); - } - } - - // push the last set of new_declarations if there were any - if (new_declarations.length > 0) { - rv.push(b.variableDeclaration(n.kind, new_declarations)); - } - - if (rv.length === 0) { - rv = b.emptyStatement(); - } - return rv; - } - - visitProperty(n) { - if (n.computed) n.key = this.visit(n.key); - n.value = this.visit(n.value); - return n; - } - - visitBlock(n) { - if (!n.ejs_env) return super.visitBlock(n); - - let this_env_id = b.identifier(`%env_${n.ejs_env.id}`); - let parent_env_name; - if (n.ejs_env.parent) parent_env_name = `%env_${n.ejs_env.parent.id}`; - - let env_prepends = []; - let new_mapping = shallow_copy_object(this.currentMapping()); - - if (n.ejs_env.closed.empty() && !n.ejs_env.nested_requires_env) { - env_prepends.push(b.letDeclaration(this_env_id, b.nullLit())); - } else { - // insert environment creation (at the start of the block) - env_prepends.push( - b.letDeclaration( - this_env_id, - intrinsic(makeClosureEnv_id, [ - b.literal(n.ejs_env.closed.size() + (n.ejs_env.parent ? 1 : 0)), - ]) - ) - ); - - n.ejs_env.slot_mapping = Object.create(null); - var i = 0; - if (n.ejs_env.parent) { - n.ejs_env.slot_mapping[parent_env_name] = i; - i += 1; - } - n.ejs_env.closed.map((el) => { - n.ejs_env.slot_mapping[el] = i; - i += 1; - }); - - if (n.ejs_env.parent) { - let parent_env_slot = n.ejs_env.slot_mapping[parent_env_name]; - env_prepends.push( - b.expressionStatement( - intrinsic(setSlot_id, [ - this_env_id, - b.literal(parent_env_slot), - b.literal(parent_env_name), - b.identifier(parent_env_name), - ]) - ) - ); - } - // XXX here's where function handling pushes closed over parameters. i'm guessing we need special logic for incoming environment slots for loop variables? - - new_mapping["%slot_mapping"] = n.ejs_env.slot_mapping; - - var flatten_memberexp = (exp, mapping) => { - if (exp.type !== CallExpression) { - return [b.literal(mapping[exp.name])]; - } else { - return flatten_memberexp(exp.arguments[0], mapping).concat([exp.arguments[1]]); - } - }; - - let prepend_environment = (exps) => { - let obj = this_env_id; - for (let prop of exps) obj = intrinsic(slot_id, [obj, prop]); - return obj; - }; - - // if there are existing mappings prepend "%env." (a MemberExpression) to them - for (let mapped in new_mapping) { - let val = new_mapping[mapped]; - if (mapped !== "%slot_mapping") - new_mapping[mapped] = prepend_environment( - flatten_memberexp(val, n.ejs_env.slot_mapping) - ); - } - - // and add mappings for all variables in .closed from "x" to "%env.x" - new_mapping["%env"] = this_env_id; - n.ejs_env.closed.keys().forEach((sym) => { - new_mapping[sym] = intrinsic(slot_id, [ - this_env_id, - b.literal(n.ejs_env.slot_mapping[sym]), - b.literal(sym), - ]); - }); - } - - // remove all mappings for variables declared in this function - if (n.ejs_decls) { - new_mapping = reject( - new_mapping, - (sym) => n.ejs_decls.has(sym) && !n.ejs_env.closed.has(sym) - ); - } - - this.mappings.push(new_mapping); - super.visitBlock(n); - if (env_prepends.length > 0) n.body = env_prepends.concat(n.body); - this.mappings.pop(); - return n; - } - - visitFunctionBody(n) { - n.scratch_size = 0; - - // we use this instead of calling super in visitFunction because we don't want to visit parameters - // during this pass, or they'll be substituted with an %env. - - this.function_stack.push(n); - n.body = this.visit(n.body); - this.function_stack.pop(); - - return n; - } - - visitFunction(n) { - try { - // XXX this should be a let, but ejs currently pukes if we close over it. - var this_env_id = b.identifier(`%env_${n.ejs_env.id}`); - let parent_env_name; - - if (n.ejs_env.parent) parent_env_name = `%env_${n.ejs_env.parent.id}`; - - let env_prepends = []; - // XXX this should be a let, but ejs currently pukes if we close over it. - var new_mapping = shallow_copy_object(this.currentMapping()); - if (n.ejs_env.closed.empty() && !n.ejs_env.nested_requires_env) { - env_prepends.push(b.letDeclaration(this_env_id, b.nullLit())); - } else { - // insert environment creation (at the start of the function body) - env_prepends.push( - b.letDeclaration( - this_env_id, - intrinsic(makeClosureEnv_id, [ - b.literal(n.ejs_env.closed.size() + (n.ejs_env.parent ? 1 : 0)), - ]) - ) - ); - - n.ejs_env.slot_mapping = Object.create(null); - // XXX this should be a let, but ejs currently pukes if we close over it. - var i = 0; - if (n.ejs_env.parent) { - n.ejs_env.slot_mapping[parent_env_name] = i; - i++; - } - n.ejs_env.closed.map((el) => { - n.ejs_env.slot_mapping[el] = i; - i++; - }); - - if (n.ejs_env.parent) { - let parent_env_slot = n.ejs_env.slot_mapping[parent_env_name]; - env_prepends.push( - b.expressionStatement( - intrinsic(setSlot_id, [ - this_env_id, - b.literal(parent_env_slot), - b.literal(parent_env_name), - b.identifier(parent_env_name), - ]) - ) - ); - } - - // we need to push assignments of any closed over parameters into the environment at this point - for (let param of n.params) { - if (n.ejs_env.closed.has(param.name)) - env_prepends.push( - b.expressionStatement( - intrinsic(setSlot_id, [ - this_env_id, - b.literal(n.ejs_env.slot_mapping[param.name]), - b.literal(param.name), - b.identifier(param.name), - ]) - ) - ); - } - - new_mapping["%slot_mapping"] = n.ejs_env.slot_mapping; - - var flatten_memberexp = (exp, mapping) => { - if (exp.type !== CallExpression) { - return [b.literal(mapping[exp.name])]; - } else { - return flatten_memberexp(exp.arguments[0], mapping).concat([ - exp.arguments[1], - ]); - } - }; - - let prepend_environment = (exps) => { - let obj = this_env_id; - for (let prop of exps) obj = intrinsic(slot_id, [obj, prop]); - return obj; - }; - - // if there are existing mappings prepend "%env." (a MemberExpression) to them - for (let mapped in new_mapping) { - let val = new_mapping[mapped]; - if (mapped !== "%slot_mapping") - new_mapping[mapped] = prepend_environment( - flatten_memberexp(val, n.ejs_env.slot_mapping) - ); - } - - // and add mappings for all variables in .closed from "x" to "%env.x" - new_mapping["%env"] = this_env_id; - n.ejs_env.closed.keys().forEach((sym) => { - new_mapping[sym] = intrinsic(slot_id, [ - this_env_id, - b.literal(n.ejs_env.slot_mapping[sym]), - b.literal(sym), - ]); - }); - } - // remove all mappings for variables declared in this function - if (n.ejs_decls) { - new_mapping = reject( - new_mapping, - (sym) => n.ejs_decls.has(sym) && !n.ejs_env.closed.has(sym) - ); - } - - this.mappings.push(new_mapping); - this.visitFunctionBody(n); - if (env_prepends.length > 0) n.body.body = env_prepends.concat(n.body.body); - this.mappings.pop(); - - // convert function expressions to an explicit closure creation, so: - // - // function X () { ...body... } - // - // replace inline with: - // - // makeClosure(%current_env, "X", function X () { ...body... }) - - if (!n.toplevel) { - if (n.type === FunctionDeclaration) { - throw new Error("there should be no FunctionDeclarations at this point"); - } else { - // n.type is FunctionExpression - let intrinsic_args = []; - intrinsic_args.push( - n.ejs_env.parent ? b.identifier(parent_env_name) : b.nullLit() - ); - - let intrinsic_id; - if (n.id) { - intrinsic_id = makeClosure_id; - if (n.id.type === Identifier) { - intrinsic_args.push(b.literal(n.id.name)); - } else { - intrinsic_args.push(b.literal(escodegen.generate(n.id))); - } - } else { - intrinsic_id = makeAnonClosure_id; - } - - intrinsic_args.push(n); - - return intrinsic(intrinsic_id, intrinsic_args); - } - } - return n; - } catch (e) { - console.warn(`exception: ${e}`); - //console.warn "compiling the following code:" - //console.warn escodegen.generate n - throw e; - } - } - - visitCallExpression(n) { - n = super.visitCallExpression(n); - - // replace calls of the form: - // X (arg1, arg2, ...) - // - // with - // invokeClosure(X, %this, %argCount, arg1, arg2, ...); - - if (is_intrinsic(n)) return n; - this.function_stack.top.scratch_size = Math.max( - this.function_stack.top.scratch_size, - n.arguments.length - ); - return intrinsic(invokeClosure_id, [n.callee].concat(n.arguments)); - } - - visitNewExpression(n) { - n = super.visitNewExpression(n); - - // replace calls of the form: - // new X (arg1, arg2, ...) - // - // with - // invokeClosure(X, %this, %argCount, arg1, arg2, ...); - - this.function_stack.top.scratch_size = Math.max( - this.function_stack.top.scratch_size, - n.arguments.length - ); - - let rv = intrinsic(invokeClosure_id, [n.callee].concat(n.arguments)); - rv.type = NewExpression; - return rv; - } -} diff --git a/lib/runtime.js b/lib/runtime.ts similarity index 76% rename from lib/runtime.js rename to lib/runtime.ts index 5976b4bb..5353245a 100644 --- a/lib/runtime.js +++ b/lib/runtime.ts @@ -1,17 +1,30 @@ -/* -*- Mode: js2; indent-tabs-mode: nil; tab-width: 4; js2-indent-offset: 4; js2-basic-offset: 4; -*- - * vim: set ts=4 sw=4 et tw=99 ft=js: +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: */ +// The compiler's window into the C runtime: every entry declares one +// extern function (or global) with its LLVM signature. Interfaces are +// built as getter objects so a function is only declared in a module +// when something actually references it. + import * as ty from "./types"; +import * as llvm from "@llvm"; +import type { ABI } from "./abi"; + +const takes_builtins = ty.takes_builtins; +const does_not_throw = ty.does_not_throw; +const does_not_access_memory = ty.does_not_access_memory; +const only_reads_memory = ty.only_reads_memory; +const returns_ejsval_bool = ty.returns_ejsval_bool; -let takes_builtins = ty.takes_builtins; -let does_not_throw = ty.does_not_throw; -let does_not_access_memory = ty.does_not_access_memory; -let only_reads_memory = ty.only_reads_memory; -let returns_ejsval_bool = ty.returns_ejsval_bool; +// getters run with the interface object (module + abi) as `this` +export interface RuntimeContext { + module: llvm.Module; + abi: ABI; +} const runtime_interface = { - personality: function () { + personality: function (this: RuntimeContext) { return this.abi.createExternalFunction(this.module, "__ejs_personality_v0", ty.Int32, [ ty.Int32, ty.Int32, @@ -21,17 +34,17 @@ const runtime_interface = { ]); }, - module_resolve: function () { + module_resolve: function (this: RuntimeContext) { return this.abi.createExternalFunction(this.module, "_ejs_module_resolve", ty.Void, [ ty.EjsModule.pointerTo(), ]); }, - module_get: function () { + module_get: function (this: RuntimeContext) { return this.abi.createExternalFunction(this.module, "_ejs_module_get", ty.EjsValue, [ ty.EjsValue, ]); }, - module_get_slot_ref: function () { + module_get_slot_ref: function (this: RuntimeContext) { return this.abi.createExternalFunction( this.module, "_ejs_module_get_slot_ref", @@ -39,7 +52,7 @@ const runtime_interface = { [ty.EjsModule.pointerTo(), ty.Int32] ); }, - module_add_export_accessors: function () { + module_add_export_accessors: function (this: RuntimeContext) { return this.abi.createExternalFunction( this.module, "_ejs_module_add_export_accessors", @@ -53,7 +66,7 @@ const runtime_interface = { ); }, - invoke_closure: function () { + invoke_closure: function (this: RuntimeContext) { return takes_builtins( this.abi.createExternalFunction(this.module, "_ejs_invoke_closure", ty.EjsValue, [ ty.EjsValue, @@ -64,7 +77,7 @@ const runtime_interface = { ]) ); }, - construct_closure: function () { + construct_closure: function (this: RuntimeContext) { return takes_builtins( this.abi.createExternalFunction(this.module, "_ejs_construct_closure", ty.EjsValue, [ ty.EjsValue, @@ -75,7 +88,7 @@ const runtime_interface = { ]) ); }, - construct_closure_apply: function () { + construct_closure_apply: function (this: RuntimeContext) { return takes_builtins( this.abi.createExternalFunction( this.module, @@ -92,7 +105,7 @@ const runtime_interface = { ); }, - set_constructor_kind_derived: function () { + set_constructor_kind_derived: function (this: RuntimeContext) { return this.abi.createExternalFunction( this.module, "_ejs_function_set_derived_constructor", @@ -100,7 +113,7 @@ const runtime_interface = { [ty.EjsValue] ); }, - set_constructor_kind_base: function () { + set_constructor_kind_base: function (this: RuntimeContext) { return this.abi.createExternalFunction( this.module, "_ejs_function_set_base_constructor", @@ -109,14 +122,14 @@ const runtime_interface = { ); }, - make_closure: function () { + make_closure: function (this: RuntimeContext) { return this.abi.createExternalFunction(this.module, "_ejs_function_new", ty.EjsValue, [ ty.EjsValue, ty.EjsValue, ty.getEjsClosureFunc(this.abi), ]); }, - make_closure_noenv: function () { + make_closure_noenv: function (this: RuntimeContext) { return this.abi.createExternalFunction( this.module, "_ejs_function_new_without_env", @@ -124,19 +137,19 @@ const runtime_interface = { [ty.EjsValue, ty.getEjsClosureFunc(this.abi)] ); }, - make_anon_closure: function () { + make_anon_closure: function (this: RuntimeContext) { return this.abi.createExternalFunction(this.module, "_ejs_function_new_anon", ty.EjsValue, [ ty.EjsValue, ty.getEjsClosureFunc(this.abi), ]); }, - make_closure_env: function () { + make_closure_env: function (this: RuntimeContext) { return this.abi.createExternalFunction(this.module, "_ejs_closureenv_new", ty.EjsValue, [ ty.Int32, ]); }, - get_env_slot_val: function () { + get_env_slot_val: function (this: RuntimeContext) { return this.abi.createExternalFunction( this.module, "_ejs_closureenv_get_slot", @@ -144,7 +157,7 @@ const runtime_interface = { [ty.EjsValue, ty.Int32] ); }, - get_env_slot_ref: function () { + get_env_slot_ref: function (this: RuntimeContext) { return this.abi.createExternalFunction( this.module, "_ejs_closureenv_get_slot_ref", @@ -153,24 +166,51 @@ const runtime_interface = { ); }, - make_generator: function () { + // the out-of-line half of the emitted write barrier + // (object-remembering: the OWNER ejsval, not the slot) + gc_write_barrier: function (this: RuntimeContext) { + return this.abi.createExternalFunction( + this.module, + "_ejs_gc_remember_val", + ty.Void, + [ty.EjsValue, ty.EjsValue] + ); + }, + + make_generator: function (this: RuntimeContext) { return this.abi.createExternalFunction(this.module, "_ejs_generator_new", ty.EjsValue, [ ty.EjsValue, ]); }, - generator_yield: function () { + generator_is_return_sentinel: function (this: RuntimeContext) { + return this.abi.createExternalFunction( + this.module, + "_ejs_generator_is_return_sentinel", + ty.EjsValue, + [ty.EjsValue] + ); + }, + generator_return_value: function (this: RuntimeContext) { + return this.abi.createExternalFunction( + this.module, + "_ejs_generator_return_value", + ty.EjsValue, + [ty.EjsValue] + ); + }, + generator_yield: function (this: RuntimeContext) { return this.abi.createExternalFunction(this.module, "_ejs_generator_yield", ty.EjsValue, [ ty.EjsValue, ty.EjsValue, ]); }, - object_create: function () { + object_create: function (this: RuntimeContext) { return this.abi.createExternalFunction(this.module, "_ejs_object_create", ty.EjsValue, [ ty.EjsValue, ]); }, - arguments_new: function () { + arguments_new: function (this: RuntimeContext) { return does_not_throw( this.abi.createExternalFunction(this.module, "_ejs_arguments_new", ty.EjsValue, [ ty.Int32, @@ -178,19 +218,19 @@ const runtime_interface = { ]) ); }, - array_new: function () { + array_new: function (this: RuntimeContext) { return this.abi.createExternalFunction(this.module, "_ejs_array_new", ty.EjsValue, [ ty.Int64, ty.Bool, ]); }, - array_new_copy: function () { + array_new_copy: function (this: RuntimeContext) { return this.abi.createExternalFunction(this.module, "_ejs_array_new_copy", ty.EjsValue, [ ty.Int64, ty.EjsValue.pointerTo(), ]); }, - array_from_iterables: function () { + array_from_iterables: function (this: RuntimeContext) { return this.abi.createExternalFunction( this.module, "_ejs_array_from_iterables", @@ -198,7 +238,17 @@ const runtime_interface = { [ty.Int32, ty.EjsValue.pointerTo()] ); }, - number_new: function () { + arg_length: function (this: RuntimeContext) { + return does_not_throw( + does_not_access_memory( + this.abi.createExternalFunction(this.module, "_ejs_arg_length", ty.EjsValue, [ + ty.Int32, + ty.Int32, + ]) + ) + ); + }, + number_new: function (this: RuntimeContext) { return does_not_throw( does_not_access_memory( this.abi.createExternalFunction(this.module, "_ejs_number_new", ty.EjsValue, [ @@ -207,7 +257,7 @@ const runtime_interface = { ) ); }, - string_new_utf8: function () { + string_new_utf8: function (this: RuntimeContext) { return only_reads_memory( does_not_throw( this.abi.createExternalFunction(this.module, "_ejs_string_new_utf8", ty.EjsValue, [ @@ -216,27 +266,27 @@ const runtime_interface = { ) ); }, - regexp_new_utf8: function () { + regexp_new_utf8: function (this: RuntimeContext) { return this.abi.createExternalFunction(this.module, "_ejs_regexp_new_utf8", ty.EjsValue, [ ty.String, ty.String, ]); }, - truthy: function () { + truthy: function (this: RuntimeContext) { return does_not_throw( does_not_access_memory( this.abi.createExternalFunction(this.module, "_ejs_truthy", ty.Bool, [ty.EjsValue]) ) ); }, - object_setprop: function () { + object_setprop: function (this: RuntimeContext) { return this.abi.createExternalFunction(this.module, "_ejs_object_setprop", ty.EjsValue, [ ty.EjsValue, ty.EjsValue, ty.EjsValue, ]); }, - object_getprop: function () { + object_getprop: function (this: RuntimeContext) { return only_reads_memory( this.abi.createExternalFunction(this.module, "_ejs_object_getprop", ty.EjsValue, [ ty.EjsValue, @@ -244,13 +294,30 @@ const runtime_interface = { ]) ); }, - global_setprop: function () { + // born-with-shape: batched literal allocation and + // fenced-constructor prefix fill. argc, names*, values*. + object_new_shaped: function (this: RuntimeContext) { + return this.abi.createExternalFunction(this.module, "_ejs_object_new_shaped", ty.EjsValue, [ + ty.Int32, + ty.EjsValue.pointerTo(), + ty.EjsValue.pointerTo(), + ]); + }, + object_fill_shaped: function (this: RuntimeContext) { + return this.abi.createExternalFunction( + this.module, + "_ejs_object_fill_shaped", + ty.EjsValue, + [ty.EjsValue, ty.Int32, ty.EjsValue.pointerTo(), ty.EjsValue.pointerTo()] + ); + }, + global_setprop: function (this: RuntimeContext) { return this.abi.createExternalFunction(this.module, "_ejs_global_setprop", ty.EjsValue, [ ty.EjsValue, ty.EjsValue, ]); }, - global_getprop: function () { + global_getprop: function (this: RuntimeContext) { return only_reads_memory( this.abi.createExternalFunction(this.module, "_ejs_global_getprop", ty.EjsValue, [ ty.EjsValue, @@ -258,7 +325,7 @@ const runtime_interface = { ); }, - object_define_accessor_prop: function () { + object_define_accessor_prop: function (this: RuntimeContext) { return this.abi.createExternalFunction( this.module, "_ejs_object_define_accessor_property", @@ -266,7 +333,15 @@ const runtime_interface = { [ty.EjsValue, ty.EjsValue, ty.EjsValue, ty.EjsValue, ty.Int32] ); }, - object_define_value_prop: function () { + object_define_accessor_prop_desc: function (this: RuntimeContext) { + return this.abi.createExternalFunction( + this.module, + "_ejs_object_define_accessor_property_desc", + ty.Bool, + [ty.EjsValue, ty.EjsValue, ty.EjsValue, ty.EjsValue, ty.Int32] + ); + }, + object_define_value_prop: function (this: RuntimeContext) { return this.abi.createExternalFunction( this.module, "_ejs_object_define_value_property", @@ -275,12 +350,20 @@ const runtime_interface = { ); }, - object_freeze: function () { + object_freeze: function (this: RuntimeContext) { return this.abi.createExternalFunction(this.module, "_ejs_object_freeze", ty.EjsValue, [ ty.EjsValue, ]); }, - object_set_prototype_of: function () { + object_literal_set_proto: function (this: RuntimeContext) { + return this.abi.createExternalFunction( + this.module, + "_ejs_object_literal_set_proto", + ty.EjsValue, + [ty.EjsValue, ty.EjsValue] + ); + }, + object_set_prototype_of: function (this: RuntimeContext) { return this.abi.createExternalFunction( this.module, "_ejs_object_set_prototype_of", @@ -288,7 +371,7 @@ const runtime_interface = { [ty.EjsValue, ty.EjsValue] ); }, - prop_iterator_new: function () { + prop_iterator_new: function (this: RuntimeContext) { return this.abi.createExternalFunction( this.module, "_ejs_property_iterator_new", @@ -296,7 +379,7 @@ const runtime_interface = { [ty.EjsValue] ); }, - prop_iterator_current: function () { + prop_iterator_current: function (this: RuntimeContext) { return this.abi.createExternalFunction( this.module, "_ejs_property_iterator_current", @@ -304,7 +387,7 @@ const runtime_interface = { [ty.EjsPropIterator] ); }, - prop_iterator_next: function () { + prop_iterator_next: function (this: RuntimeContext) { return this.abi.createExternalFunction( this.module, "_ejs_property_iterator_next", @@ -312,15 +395,15 @@ const runtime_interface = { [ty.EjsPropIterator, ty.Bool] ); }, - begin_catch: function () { + begin_catch: function (this: RuntimeContext) { return this.abi.createExternalFunction(this.module, "_ejs_begin_catch", ty.EjsValue, [ ty.Int8Pointer, ]); }, - end_catch: function () { + end_catch: function (this: RuntimeContext) { return this.abi.createExternalFunction(this.module, "_ejs_end_catch", ty.EjsValue, []); }, - throw_nativeerror_utf8: function () { + throw_nativeerror_utf8: function (this: RuntimeContext) { return this.abi.createExternalFunction( this.module, "_ejs_throw_nativeerror_utf8", @@ -328,23 +411,35 @@ const runtime_interface = { [ty.Int32, ty.String] ); }, - throw: function () { + throw: function (this: RuntimeContext) { return this.abi.createExternalFunction(this.module, "_ejs_throw", ty.Void, [ty.EjsValue]); }, - rethrow: function () { + rethrow: function (this: RuntimeContext) { return this.abi.createExternalFunction(this.module, "_ejs_rethrow", ty.Void, [ty.EjsValue]); }, - ToString: function () { + ToString: function (this: RuntimeContext) { return this.abi.createExternalFunction(this.module, "ToString", ty.EjsValue, [ty.EjsValue]); }, - string_concat: function () { + string_concat: function (this: RuntimeContext) { return this.abi.createExternalFunction(this.module, "_ejs_string_concat", ty.EjsValue, [ ty.EjsValue, ty.EjsValue, ]); }, - init_string_literal: function () { + // module-init interning of guard shapes (names are + // this module's atoms; f64_mask bit i = field i has repr f64). + // Returns the interned shape index, or EJS_SHAPE_NOMATCH. + shape_intern: function (this: RuntimeContext) { + return does_not_throw( + this.abi.createExternalFunction(this.module, "_ejs_shape_intern", ty.Int32, [ + ty.Int32, + ty.EjsValue.pointerTo(), + ty.Int32, + ]) + ); + }, + init_string_literal: function (this: RuntimeContext) { return this.abi.createExternalFunction(this.module, "_ejs_string_init_literal", ty.Void, [ ty.String, ty.EjsValue.pointerTo(), @@ -354,12 +449,12 @@ const runtime_interface = { ]); }, - gc_add_root: function () { + gc_add_root: function (this: RuntimeContext) { return this.abi.createExternalFunction(this.module, "_ejs_gc_add_root", ty.Void, [ ty.EjsValue.pointerTo(), ]); }, - typeof_is_object: function () { + typeof_is_object: function (this: RuntimeContext) { return returns_ejsval_bool( only_reads_memory( this.abi.createExternalFunction( @@ -371,7 +466,7 @@ const runtime_interface = { ) ); }, - typeof_is_function: function () { + typeof_is_function: function (this: RuntimeContext) { return returns_ejsval_bool( only_reads_memory( this.abi.createExternalFunction( @@ -383,7 +478,7 @@ const runtime_interface = { ) ); }, - typeof_is_string: function () { + typeof_is_string: function (this: RuntimeContext) { return returns_ejsval_bool( only_reads_memory( this.abi.createExternalFunction( @@ -395,7 +490,7 @@ const runtime_interface = { ) ); }, - typeof_is_number: function () { + typeof_is_number: function (this: RuntimeContext) { return returns_ejsval_bool( only_reads_memory( this.abi.createExternalFunction( @@ -407,7 +502,7 @@ const runtime_interface = { ) ); }, - typeof_is_undefined: function () { + typeof_is_undefined: function (this: RuntimeContext) { return returns_ejsval_bool( only_reads_memory( this.abi.createExternalFunction( @@ -419,7 +514,7 @@ const runtime_interface = { ) ); }, - typeof_is_null: function () { + typeof_is_null: function (this: RuntimeContext) { return returns_ejsval_bool( only_reads_memory( this.abi.createExternalFunction( @@ -431,7 +526,7 @@ const runtime_interface = { ) ); }, - typeof_is_boolean: function () { + typeof_is_boolean: function (this: RuntimeContext) { return returns_ejsval_bool( only_reads_memory( this.abi.createExternalFunction( @@ -444,7 +539,7 @@ const runtime_interface = { ); }, - create_iter_result: function () { + create_iter_result: function (this: RuntimeContext) { return this.abi.createExternalFunction( this.module, "_ejs_create_iter_result", @@ -453,7 +548,7 @@ const runtime_interface = { ); }, - iterator_wrapper_new: function () { + iterator_wrapper_new: function (this: RuntimeContext) { return this.abi.createExternalFunction( this.module, "_ejs_iterator_wrapper_new", @@ -462,85 +557,85 @@ const runtime_interface = { ); }, - undefined: function () { + undefined: function (this: RuntimeContext) { return this.module.getOrInsertGlobal("_ejs_undefined", ty.EjsValue); }, - true: function () { + true: function (this: RuntimeContext) { return this.module.getOrInsertGlobal("_ejs_true", ty.EjsValue); }, - false: function () { + false: function (this: RuntimeContext) { return this.module.getOrInsertGlobal("_ejs_false", ty.EjsValue); }, - null: function () { + null: function (this: RuntimeContext) { return this.module.getOrInsertGlobal("_ejs_null", ty.EjsValue); }, - one: function () { + one: function (this: RuntimeContext) { return this.module.getOrInsertGlobal("_ejs_one", ty.EjsValue); }, - zero: function () { + zero: function (this: RuntimeContext) { return this.module.getOrInsertGlobal("_ejs_zero", ty.EjsValue); }, - global: function () { + global: function (this: RuntimeContext) { return this.module.getOrInsertGlobal("_ejs_global", ty.EjsValue); }, - exception_typeinfo: function () { + exception_typeinfo: function (this: RuntimeContext) { return this.module.getOrInsertGlobal("EJS_EHTYPE_ejsvalue", ty.EjsExceptionTypeInfo); }, - function_specops: function () { + function_specops: function (this: RuntimeContext) { return this.module.getOrInsertGlobal("_ejs_Function_specops", ty.EjsSpecops); }, - symbol_specops: function () { + symbol_specops: function (this: RuntimeContext) { return this.module.getOrInsertGlobal("_ejs_Symbol_specops", ty.EjsSpecops); }, - "unop-": function () { + "unop-": function (this: RuntimeContext) { return this.abi.createExternalFunction(this.module, "_ejs_op_neg", ty.EjsValue, [ ty.EjsValue, ]); }, - "unop+": function () { + "unop+": function (this: RuntimeContext) { return this.abi.createExternalFunction(this.module, "_ejs_op_plus", ty.EjsValue, [ ty.EjsValue, ]); }, - "unop!": function () { + "unop!": function (this: RuntimeContext) { return returns_ejsval_bool( this.abi.createExternalFunction(this.module, "_ejs_op_not", ty.EjsValue, [ty.EjsValue]) ); }, - "unop~": function () { + "unop~": function (this: RuntimeContext) { return this.abi.createExternalFunction(this.module, "_ejs_op_bitwise_not", ty.EjsValue, [ ty.EjsValue, ]); }, - unoptypeof: function () { + unoptypeof: function (this: RuntimeContext) { return does_not_throw( this.abi.createExternalFunction(this.module, "_ejs_op_typeof", ty.EjsValue, [ ty.EjsValue, ]) ); }, - unopdelete: function () { + unopdelete: function (this: RuntimeContext) { return this.abi.createExternalFunction(this.module, "_ejs_op_delete", ty.EjsValue, [ ty.EjsValue, ty.EjsValue, ]); }, // this is a unop, but ours only works for memberexpressions - unopvoid: function () { + unopvoid: function (this: RuntimeContext) { return this.abi.createExternalFunction(this.module, "_ejs_op_void", ty.EjsValue, [ ty.EjsValue, ]); }, - dump_value: function () { + dump_value: function (this: RuntimeContext) { return this.abi.createExternalFunction(this.module, "_ejs_dump_value", ty.Void, [ ty.EjsValue, ]); }, - log: function () { + log: function (this: RuntimeContext) { return this.abi.createExternalFunction(this.module, "_ejs_logstr", ty.Void, [ty.String]); }, - record_binop: function () { + record_binop: function (this: RuntimeContext) { return this.abi.createExternalFunction(this.module, "_ejs_record_binop", ty.Void, [ ty.Int32, ty.String, @@ -548,20 +643,20 @@ const runtime_interface = { ty.EjsValue, ]); }, - record_assignment: function () { + record_assignment: function (this: RuntimeContext) { return this.abi.createExternalFunction(this.module, "_ejs_record_assignment", ty.Void, [ ty.Int32, ty.EjsValue, ]); }, - record_getprop: function () { + record_getprop: function (this: RuntimeContext) { return this.abi.createExternalFunction(this.module, "_ejs_record_getprop", ty.Void, [ ty.Int32, ty.EjsValue, ty.EjsValue, ]); }, - record_setprop: function () { + record_setprop: function (this: RuntimeContext) { return this.abi.createExternalFunction(this.module, "_ejs_record_setprop", ty.Void, [ ty.Int32, ty.EjsValue, @@ -569,21 +664,25 @@ const runtime_interface = { ty.EjsValue, ]); }, +} satisfies Record llvm.EjsFunction | llvm.GlobalVariable>; + +export type RuntimeInterface = RuntimeContext & { + readonly [K in keyof typeof runtime_interface]: ReturnType<(typeof runtime_interface)[K]>; }; -export function createInterface(module, abi) { - let runtime = { - module: module, - abi: abi, - }; +export function createInterface(module: llvm.Module, abi: ABI): RuntimeInterface { + const runtime = { module, abi } as RuntimeInterface; - for (let k of Object.keys(runtime_interface)) + for (const k of Object.keys(runtime_interface) as (keyof typeof runtime_interface)[]) Object.defineProperty(runtime, k, { get: runtime_interface[k] }); return runtime; } -export function createBinopsInterface(module, abi) { - let createBinop = (n) => +export function createBinopsInterface( + module: llvm.Module, + abi: ABI +): Record { + const createBinop = (n: string) => abi.createExternalFunction(module, n, ty.EjsValue, [ty.EjsValue, ty.EjsValue]); return Object.create(null, { "^": { get: () => createBinop("_ejs_op_bitwise_xor") }, @@ -617,8 +716,8 @@ export function createBinopsInterface(module, abi) { }); } -export function createAtomsInterface(module) { - let getGlobal = (n) => module.getOrInsertGlobal(n, ty.EjsValue); +export function createAtomsInterface(module: llvm.Module): Record { + const getGlobal = (n: string) => module.getOrInsertGlobal(n, ty.EjsValue); return Object.create(null, { null: { get: () => getGlobal("_ejs_atom_null") }, undefined: { get: () => getGlobal("_ejs_atom_undefined") }, @@ -784,8 +883,8 @@ export function createAtomsInterface(module) { }); } -export function createGlobalsInterface(module) { - let getGlobal = (n) => module.getOrInsertGlobal(n, ty.EjsValue); +export function createGlobalsInterface(module: llvm.Module): Record { + const getGlobal = (n: string) => module.getOrInsertGlobal(n, ty.EjsValue); return Object.create(null, { Object: { get: () => getGlobal("_ejs_Object") }, Object_prototype: { get: () => getGlobal("_ejs_Object_prototype") }, @@ -850,8 +949,8 @@ export function createGlobalsInterface(module) { }); } -export function createSymbolsInterface(module) { - let getGlobal = (n) => module.getOrInsertGlobal(n, ty.EjsValue); +export function createSymbolsInterface(module: llvm.Module): Record { + const getGlobal = (n: string) => module.getOrInsertGlobal(n, ty.EjsValue); return Object.create(null, { create: { get: () => getGlobal("_ejs_Symbol_create") }, }); diff --git a/lib/sret-abi.js b/lib/sret-abi.js deleted file mode 100644 index 778c206b..00000000 --- a/lib/sret-abi.js +++ /dev/null @@ -1,120 +0,0 @@ -/* -*- Mode: js2; tab-width: 4; indent-tabs-mode: nil; -*- - * vim: set ts=4 sw=4 et tw=99 ft=js: - */ - -import * as llvm from "@llvm"; -import * as types from "./types"; -import * as consts from "./consts"; -import { ABI } from "./abi"; - -let ir = llvm.IRBuilder; - -// armv7/x86 requires us to pass a pointer to a stack slot for the return value when it's EjsValue. -// so functions that would normally be defined as: -// -// ejsval _ejs_normal_func (ejsval env, ejsval this, uint32_t argc, ejsval* args) -// -// are instead expressed as: -// -// void _ejs_sret_func (ejsval* sret, ejsval env, ejsval this, uint32_t argc, ejsval* args) -// -export class SRetABI extends ABI { - constructor() { - super(); - this.ejs_return_type = types.Void; - this.ejs_params.unshift({ - name: "%retval", - llvm_type: types.EjsValue.pointerTo(), - }); - this.env_param_index += 1; - this.this_param_index += 1; - this.argc_param_index += 1; - this.args_param_index += 1; - this.newTarget_param_index += 1; - } - - createCall(fromFunction, calleeType, callee, argv, callname) { - if (callee.hasStructRetAttr()) { - let sret_alloca = this.createAlloca(fromFunction, types.EjsValue, "sret"); - argv.unshift(sret_alloca); - - //sret_as_i8 = ir.createBitCast sret_alloca, types.Int8Pointer, "sret_as_i8" - //ir.createLifetimeStart sret_as_i8, consts.int64(8) //sizeof(ejsval) - let call = super.createCall(fromFunction, calleeType, callee, argv, ""); - call.setStructRet(); - - let rv = ir.createLoad(sret_alloca, callname); - //ir.createLifetimeEnd sret_as_i8, consts.int64(8) //sizeof(ejsval) - return rv; - } else { - return super.createCall(fromFunction, calleeType, callee, argv, callname); - } - } - - createInvoke(fromFunction, calleeType, callee, argv, normal_block, exc_block, callname) { - if (callee.hasStructRetAttr()) { - let sret_alloca = this.createAlloca(fromFunction, types.EjsValue, "sret"); - argv.unshift(sret_alloca); - - //sret_as_i8 = ir.createBitCast sret_alloca, types.Int8Pointer, "sret_as_i8" - //ir.createLifetimeStart sret_as_i8, consts.int64(8) //sizeof(ejsval) - let call = super.createInvoke( - fromFunction, - calleeType, - callee, - argv, - normal_block, - exc_block, - "" - ); - call.setStructRet(); - - ir.setInsertPoint(normal_block); - let rv = ir.createLoad(sret_alloca, callname); - //ir.createLifetimeEnd sret_as_i8, consts.int64(8) //sizeof(ejsval) - return rv; - } else { - return super.createInvoke( - fromFunction, - calleeType, - callee, - argv, - normal_block, - exc_block, - callname - ); - } - } - - createRet(fromFunction, value) { - ir.createStore(value, fromFunction.args[0]); - return ir.createRetVoid(); - } - - createExternalFunction(inModule, name, ret_type, param_types) { - return this.createFunction(inModule, name, ret_type, param_types, true); - } - - createFunction(inModule, name, ret_type, param_types, external = false) { - let sret = false; - let rv; - if (ret_type === types.EjsValue) { - param_types.unshift(ret_type.pointerTo()); - ret_type = types.Void; - sret = true; - } - if (external) rv = inModule.getOrInsertExternalFunction(name, ret_type, param_types); - else rv = inModule.getOrInsertFunction(name, ret_type, param_types); - - if (sret) rv.setStructRet(); - return rv; - } - - createFunctionType(ret_type, param_types) { - if (ret_type === types.EjsValue) { - param_types.unshift(ret_type.pointerTo()); - ret_type = types.Void; - } - return super.createFunctionType(ret_type, param_types); - } -} diff --git a/lib/sret-abi.ts b/lib/sret-abi.ts new file mode 100644 index 00000000..0f5df3e2 --- /dev/null +++ b/lib/sret-abi.ts @@ -0,0 +1,144 @@ +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: + */ + +import * as llvm from "@llvm"; +import * as types from "./types"; +import { ABI } from "./abi"; + +const ir = llvm.IRBuilder; + +// armv7/x86 requires us to pass a pointer to a stack slot for the return +// value when it's EjsValue. so functions that would normally be defined +// as: +// +// ejsval _ejs_normal_func (ejsval env, ejsval this, uint32_t argc, ejsval* args) +// +// are instead expressed as: +// +// void _ejs_sret_func (ejsval* sret, ejsval env, ejsval this, uint32_t argc, ejsval* args) +// +export class SRetABI extends ABI { + constructor() { + super(); + this.ejs_return_type = types.Void; + this.ejs_params.unshift({ + name: "%retval", + llvm_type: types.EjsValue.pointerTo(), + }); + this.env_param_index += 1; + this.this_param_index += 1; + this.argc_param_index += 1; + this.args_param_index += 1; + this.newTarget_param_index += 1; + } + + override createCall( + fromFunction: llvm.EjsFunction, + calleeType: llvm.FunctionType, + callee: llvm.Value, + argv: llvm.Value[], + callname: string + ): llvm.Value { + if (calleeHasStructRet(callee)) { + const sret_alloca = this.createAlloca(fromFunction, types.EjsValue, "sret"); + argv.unshift(sret_alloca); + + const call = super.createCall(fromFunction, calleeType, callee, argv, ""); + (call as llvm.CallInst).setStructRet(); + + return ir.createLoad(types.EjsValue, sret_alloca, callname); + } + return super.createCall(fromFunction, calleeType, callee, argv, callname); + } + + override createInvoke( + fromFunction: llvm.EjsFunction, + calleeType: llvm.FunctionType, + callee: llvm.Value, + argv: llvm.Value[], + normal_block: llvm.BasicBlock, + exc_block: llvm.BasicBlock, + callname: string + ): llvm.Value { + if (calleeHasStructRet(callee)) { + const sret_alloca = this.createAlloca(fromFunction, types.EjsValue, "sret"); + argv.unshift(sret_alloca); + + const call = super.createInvoke( + fromFunction, + calleeType, + callee, + argv, + normal_block, + exc_block, + "" + ); + (call as llvm.CallInst).setStructRet(); + + ir.setInsertPoint(normal_block); + return ir.createLoad(types.EjsValue, sret_alloca, callname); + } + return super.createInvoke( + fromFunction, + calleeType, + callee, + argv, + normal_block, + exc_block, + callname + ); + } + + override createRet(fromFunction: llvm.EjsFunction, value: llvm.Value): llvm.Value { + ir.createStore(value, fromFunction.args[0]!); + return ir.createRetVoid(); + } + + override createExternalFunction( + inModule: llvm.Module, + name: string, + ret_type: llvm.Type, + param_types: llvm.Type[] + ): llvm.EjsFunction { + return this.createFunction(inModule, name, ret_type, param_types, true); + } + + override createFunction( + inModule: llvm.Module, + name: string, + ret_type: llvm.Type, + param_types: llvm.Type[], + external = false + ): llvm.EjsFunction { + let sret = false; + if (ret_type === types.EjsValue) { + param_types.unshift(ret_type.pointerTo()); + ret_type = types.Void; + sret = true; + } + const rv = external + ? inModule.getOrInsertExternalFunction(name, ret_type, param_types) + : inModule.getOrInsertFunction(name, ret_type, param_types); + + if (sret) rv.setStructRet(); + return rv; + } + + override createFunctionType(ret_type: llvm.Type, param_types: llvm.Type[]): llvm.FunctionType { + if (ret_type === types.EjsValue) { + param_types.unshift(ret_type.pointerTo()); + ret_type = types.Void; + } + return super.createFunctionType(ret_type, param_types); + } +} + +// callees arrive as plain Values (function pointers or functions); only +// actual functions carry the sret attribute +function calleeHasStructRet(callee: llvm.Value): callee is llvm.EjsFunction { + return ( + typeof (callee as llvm.EjsFunction).hasStructRetAttr === "function" && + (callee as llvm.EjsFunction).hasStructRetAttr() + ); +} diff --git a/lib/stack-es6.js b/lib/stack-es6.js deleted file mode 100644 index ea483712..00000000 --- a/lib/stack-es6.js +++ /dev/null @@ -1,28 +0,0 @@ -/* -*- Mode: js2; tab-width: 4; indent-tabs-mode: nil; -*- - * vim: set ts=4 sw=4 et tw=99 ft=js: - */ -export class Stack { - constructor(initial) { - this.stack = []; - if (initial) this.stack.unshift(initial); - } - - push(o) { - this.stack.unshift(o); - } - - pop() { - if (this.stack.length === 0) throw new Error("Stack is empty"); - return this.stack.shift(); - } - - // add a 'top' property to make things a little clearer/nicer to read in the compiler - get top() { - if (this.stack.length === 0) throw new Error("Stack is empty"); - return this.stack[0]; - } - - get depth() { - return this.stack.length; - } -} diff --git a/lib/stack-es6.ts b/lib/stack-es6.ts new file mode 100644 index 00000000..ca847ae0 --- /dev/null +++ b/lib/stack-es6.ts @@ -0,0 +1,31 @@ +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: + */ + +export class Stack { + stack: T[] = []; + + constructor(initial?: T) { + if (initial !== undefined) this.stack.unshift(initial); + } + + push(o: T): void { + this.stack.unshift(o); + } + + pop(): T { + const top = this.stack.shift(); + if (top === undefined) throw new Error("Stack is empty"); + return top; + } + + // a 'top' property makes things a little clearer/nicer to read + get top(): T { + if (this.stack.length === 0) throw new Error("Stack is empty"); + return this.stack[0]!; + } + + get depth(): number { + return this.stack.length; + } +} diff --git a/lib/stack.js b/lib/stack.js deleted file mode 100644 index 14fe2bfb..00000000 --- a/lib/stack.js +++ /dev/null @@ -1,35 +0,0 @@ -(function () { - exports.Stack = (function () { - function Stack(initial) { - this.stack = []; - if (initial) this.stack.unshift(initial); - } - - Stack.prototype.push = function (o) { - this.stack.unshift(o); - }; - - Stack.prototype.pop = function () { - if (this.stack.length === 0) throw new Error("Stack is empty"); - return this.stack.shift(); - }; - - // add a 'top' property to make things a little clearer/nicer to read in the compiler - - Object.defineProperty(Stack.prototype, "top", { - get: function () { - if (this.stack.length === 0) throw new Error("Stack is empty"); - return this.stack[0]; - }, - }); - - // and a 'depth' property - Object.defineProperty(Stack.prototype, "depth", { - get: function () { - return this.stack.length; - }, - }); - - return Stack; - })(); -})(); diff --git a/lib/triple.js b/lib/triple.js deleted file mode 100644 index 05a71766..00000000 --- a/lib/triple.js +++ /dev/null @@ -1,112 +0,0 @@ -import * as os from "@node-compat/os"; -import { ABI } from "./abi"; -import { SRetABI } from "./sret-abi"; - -export class Triple { - constructor(arch, vendor, os) { - this.arch = arch; - this.vendor = vendor; - this.os = os; - } - - toString() { - return `${this.arch}-${this.vendor}-${this.os}`; - } - - isLittleEndian() { - switch (this.arch) { - case "x86_64": - case "x86": - case "arm": - case "arm64": - return true; - default: - throw new Error(`unknown endianness for arch: ${this.arch}`); - } - } - - pointerSize() { - switch (this.arch) { - case "x86_64": - case "arm64": - return 64; - case "x86": - case "arm": - return 32; - default: - throw new Error(`unknown pointer size for arch: ${this.arch}`); - } - } - - llcArch() { - switch (this.arch) { - case "x86_64": - return "x86-64"; - case "x86": - return "x86"; - case "arm64": - return "arm64"; - case "arm": - return "arm"; - default: - throw new Error(`unknown llc arch for arch: ${this.arch}`); - } - } - - clangArch() { - switch (this.arch) { - case "x86_64": - return "x86_64"; - case "x86": - return "i386"; - case "arm64": - return "arm64"; - case "arm": - return "armv7"; - default: - throw new Error(`unknown clang arch for arch: ${this.arch}`); - } - } - - abi() { - switch (this.arch) { - case "x86_64": - case "arm64": - return new ABI(); - case "x86": - case "arm": - return new SRetABI(); - default: - throw new Error(`unknown abi for arch: ${this.arch}`); - } - } - - static fromProcess() { - let vendor = "unknown";; - - let arch = os.arch(); - if (arch === "x64") arch = "x86_64"; - if (arch === "ia32") arch = "x86"; - - let platform = os.platform(); - if (platform === "darwin") { - vendor = "apple"; - } - - return new Triple(arch, vendor, platform); - } - - static fromString(str) { - let split = str.split("-"); - let arch, vendor, os; - if (split.length == 2) { - arch = "unknown"; - [vendor, os] = split; - } else if (split.length == 3) { - [arch, vendor, os] = split; - } else { - throw new Error(`invalid triple: ${str}`); - } - return new Triple(arch, vendor, os); - } -} diff --git a/lib/triple.ts b/lib/triple.ts new file mode 100644 index 00000000..25e4cab1 --- /dev/null +++ b/lib/triple.ts @@ -0,0 +1,195 @@ +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: + */ + +import * as os from "@node-compat/os"; +import { ABI } from "./abi"; +import { SRetABI } from "./sret-abi"; + +export interface TripleParts { + arch: string; + vendor: string; + os: string; + env?: string | undefined; +} + +export class Triple { + arch: string; + vendor: string; + os: string; + env: string | undefined; + + constructor({ arch, vendor, os, env }: TripleParts) { + this.arch = arch; + this.vendor = vendor; + this.os = os; + this.env = env; + } + + toString(): string { + const envSuffix = this.env ? `-${this.env}` : ""; + return `${this.arch}-${this.vendor}-${this.os}${envSuffix}`; + } + + // same as toString but we drop the vendor + toShortString(): string { + const envSuffix = this.env ? `-${this.env}` : ""; + return `${this.arch}-${this.os}${envSuffix}`; + } + + isLittleEndian(): boolean { + switch (this.arch) { + case "x86_64": + case "x86": + case "arm": + case "arm64": + case "aarch64": + return true; + default: + throw new Error(`unknown endianness for arch: ${this.arch}`); + } + } + + pointerSize(): number { + switch (this.arch) { + case "x86_64": + case "arm64": + case "aarch64": + return 64; + case "x86": + case "arm": + return 32; + default: + throw new Error(`unknown pointer size for arch: ${this.arch}`); + } + } + + // the llvm target triple for emitted modules. without this (and the + // data layout below) set on the module, opt folds struct GEPs using + // llvm's default layout, where i64 is only 4-byte aligned -- which + // computes different field offsets than the C compiler does for the + // runtime (e.g. EJSModule.exports), corrupting every module slot + // access and blinding the GC to module-referenced objects. + llvmTriple(): string { + switch (this.os) { + case "macos": + return `${this.arch}-apple-macosx`; + case "ios": + return `${this.arch}-apple-ios`; + case "linux": + return `${this.arch === "arm64" ? "aarch64" : this.arch}-unknown-linux-gnu`; + default: + throw new Error(`unknown llvm triple for os: ${this.os}`); + } + } + + // must match what clang uses for the runtime's target (see llvmTriple + // above for why). + dataLayout(): string { + if (this.os === "macos" || this.os === "ios") { + if (this.arch === "arm64" || this.arch === "aarch64") + return "e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-n32:64-S128-Fn32"; + return "e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"; + } + if (this.arch === "x86_64") + return "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"; + return "e-m:e-i8:8:32-i16:16:32-i64:64-i128:128-n32:64-S128"; + } + + llcArch(): string { + switch (this.arch) { + case "x86_64": + return "x86-64"; + case "x86": + return "x86"; + case "arm64": + return "arm64"; + case "aarch64": + return "aarch64"; + case "arm": + return "arm"; + default: + throw new Error(`unknown llc arch for arch: ${this.arch}`); + } + } + + clangArch(): string { + switch (this.arch) { + case "x86_64": + return "x86_64"; + case "x86": + return "i386"; + case "aarch64": + return "aarch64"; + case "arm64": + return "arm64"; + case "arm": + return "armv7"; + default: + throw new Error(`unknown clang arch for arch: ${this.arch}`); + } + } + + abi(): ABI { + switch (this.arch) { + case "x86_64": + case "aarch64": + case "arm64": + return new ABI(); + case "x86": + case "arm": + return new SRetABI(); + default: + throw new Error(`unknown abi for arch: ${this.arch}`); + } + } + + static fromProcess(): Triple { + let vendor = "unknown"; + + let arch: string = os.arch(); + if (arch === "x64") arch = "x86_64"; + if (arch === "ia32") arch = "x86"; + + let _os: string = os.platform(); + if (_os === "darwin") { + vendor = "apple"; + _os = "macos"; + } + + return new Triple({ arch, vendor, os: _os }); + } + + static fromString(str: string): Triple { + const split = str.split("-"); + if (split.length === 2) { + const [vendor, os] = split; + return new Triple({ arch: "unknown", vendor: vendor!, os: os! }); + } else if (split.length === 3) { + const [arch, vendor, os] = split; + return new Triple({ arch: arch!, vendor: vendor!, os: os! }); + } else if (split.length === 4) { + const [arch, vendor, os, env] = split; + return new Triple({ arch: arch!, vendor: vendor!, os: os!, env }); + } + throw new Error(`invalid triple: ${str}`); + } + + static fromShortString(str: string): Triple { + const split = str.split("-"); + let arch: string, os: string, env: string | undefined; + if (split.length === 2) { + [arch, os] = split as [string, string]; + } else if (split.length === 3) { + [arch, os, env] = split as [string, string, string]; + } else { + throw new Error(`invalid triple short string: ${str}`); + } + // try and fill in the vendor + const vendor = + os === "macos" || os === "ios" || os === "tvos" || os === "watchos" + ? "apple" + : "unknown"; + return new Triple({ arch, vendor, os, env }); + } +} diff --git a/lib/types.js b/lib/types.js deleted file mode 100644 index fffdbc75..00000000 --- a/lib/types.js +++ /dev/null @@ -1,139 +0,0 @@ -/* -*- Mode: js2; tab-width: 4; indent-tabs-mode: nil; -*- - * vim: set ts=4 sw=4 et tw=99 ft=js: - */ -import * as llvm from "@llvm"; - -export let String = llvm.Type.getInt8Ty().pointerTo(); -export let Int8Pointer = String; -export let Bool = llvm.Type.getInt8Ty(); -export let Void = llvm.Type.getVoidTy(); -export let JSChar = llvm.Type.getInt16Ty(); -export let Int1 = llvm.Type.getInt1Ty(); -export let Int32 = llvm.Type.getInt32Ty(); -export let Int64 = llvm.Type.getInt64Ty(); -export let Double = llvm.Type.getDoubleTy(); - -export let EjsLandingPad = llvm.StructType.create("EjsLandingPad", [Int8Pointer, Int32]); -export let EjsValueLayout = llvm.StructType.create("EjsValueType", [Int64]); -export let EjsValue = EjsValueLayout; - -export let EjsClosureEnv = llvm.StructType.create("struct.EJSClosureEnv", [ - Int32, - Int32, - llvm.ArrayType.get(EjsValueLayout, 1), -]); -export let EjsPropIterator = EjsValue; -//export let EjsClosureFunc = llvm.FunctionType.get(EjsValue, [EjsValue, EjsValue, Int32, EjsValue.pointerTo()]).pointerTo(); -export let EjsClosureFunc = llvm.FunctionType.get( - Void, - [EjsValue.pointerTo(), EjsValue, EjsValue.pointerTo(), Int32, EjsValue.pointerTo(), Int32], - EjsValue -).pointerTo(); -export let getEjsClosureFunc = (abi) => - abi - .createFunctionType(EjsValue, [ - EjsValue, - EjsValue.pointerTo(), - Int32, - EjsValue.pointerTo(), - EjsValue, - ]) - .pointerTo(); - -export let EjsPrimString = llvm.StructType.create("EjsPrimString", [Int32, Int32, Int64, Int64]); // XXX not the real structure but it should be good - -export let EjsSpecops = llvm.StructType.create("struct.EJSSpecOps", []); // XXX - -export let EjsPropertyMap = llvm.StructType.create("struct.EJSPropertyMap", [ - JSChar.pointerTo(), // _EJSPropertyMapSlot** slots - JSChar.pointerTo(), // _EJSPropertyMapSlot* first_insert - JSChar.pointerTo(), // _EJSPropertyMapSlot* last_insert - Int32, // int nslots; - Int32, // int inuse; -]); - -export let EjsObject = null; -export let EjsFunction = null; -export let EjsModule = null; - -function CreateModuleTy(suffix, num_exports) { - return llvm.StructType.create(`struct.EJSModule${suffix}`, [ - EjsObject, // EJSObject obj; - String, // const char* module_name - Int32, // int32_t num_exports - llvm.ArrayType.get(EjsValueLayout, num_exports), - ]); -} - -export function getModuleSpecificType(module_name, num_exports) { - return CreateModuleTy(`_${module_name}`, num_exports); -} - -export function initTypes(is32bit) { - // EJSObject's struct type depends no the pointer size of the - // architecture. on 32 bit platforms (XXX or maybe just x86?) - // clang inserts 4 bytes of padding at the end. we therefore need - // to delay initialization of EJSObject (and therefore its uses) - // until after we've determined pointer size. - - if (is32bit) { - EjsObject = llvm.StructType.create("struct.EJSObject", [ - Int32, // GCObjectHeader gc_header; - EjsSpecops.pointerTo(), // EJSSpecOps* ops; - EjsValue, // ejsval proto; // the __proto__ property - EjsPropertyMap.pointerTo(), // EJSPropertyMap map; - llvm.ArrayType.get(llvm.Type.getInt8Ty(), 4), // alignment that clang adds - ]); - } else { - EjsObject = llvm.StructType.create("struct.EJSObject", [ - Int32, // GCObjectHeader gc_header; - EjsSpecops.pointerTo(), // EJSSpecOps* ops; - EjsValue, // ejsval proto; // the __proto__ property - EjsPropertyMap.pointerTo(), // EJSPropertyMap map; - ]); - } - - EjsFunction = llvm.StructType.create("struct.EJSFunction", [ - EjsObject, // EJSObject obj; - EjsClosureFunc, // EJSClosureFunc func; - EjsValue, // ejsval env; - - Int32, // EJSBool bound; - ]); - - EjsModule = CreateModuleTy("", 1); -} - -// exception types - -// the c++ typeinfo for our exceptions -export let EjsExceptionTypeInfo = llvm.StructType.create("EjsExceptionTypeInfoType", [ - Int8Pointer, - Int8Pointer, - Int8Pointer, -]).pointerTo(); - -export function takes_builtins(n) { - n.takes_builtins = true; - return n; -} - -export function only_reads_memory(n) { - n.setOnlyReadsMemory(); - return n; -} - -export function does_not_access_memory(n) { - n.setDoesNotAccessMemory(); - return n; -} - -export function does_not_throw(n) { - n.setDoesNotThrow(); - return n; -} - -export function returns_ejsval_bool(n) { - n.returns_ejsval_bool = true; - return n; -} diff --git a/lib/types.ts b/lib/types.ts new file mode 100644 index 00000000..b23a27d9 --- /dev/null +++ b/lib/types.ts @@ -0,0 +1,164 @@ +/* -*- Mode: typescript; indent-tabs-mode: nil; tab-width: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=typescript: + */ + +import * as llvm from "@llvm"; + +export const String = llvm.Type.getInt8Ty().pointerTo(); +export const Int8Pointer = String; +export const Bool = llvm.Type.getInt8Ty(); +export const Void = llvm.Type.getVoidTy(); +export const JSChar = llvm.Type.getInt16Ty(); +export const Int1 = llvm.Type.getInt1Ty(); +export const Int32 = llvm.Type.getInt32Ty(); +export const Int64 = llvm.Type.getInt64Ty(); +export const Double = llvm.Type.getDoubleTy(); + +export const EjsLandingPad = llvm.StructType.create("EjsLandingPad", [Int8Pointer, Int32]); +export const EjsValueLayout = llvm.StructType.create("EjsValueType", [Int64]); +export const EjsValue: llvm.Type = EjsValueLayout; + +export const EjsClosureEnv = llvm.StructType.create("struct.EJSClosureEnv", [ + Int32, // GCObjectHeader gc_header (low half) + Int32, // GCObjectHeader shape/gc bits (high half) + Int32, // uint32_t length + Int32, // padding (slots are 8-aligned) + llvm.ArrayType.get(EjsValueLayout, 1), +]); +export const EjsPropIterator = EjsValue; +export const EjsClosureFunc = llvm.FunctionType.get( + Void, + [EjsValue.pointerTo(), EjsValue, EjsValue.pointerTo(), Int32, EjsValue.pointerTo(), Int32], + EjsValue +).pointerTo(); + +// the piece of the ABI interface this module needs (abi.ts imports this +// module, so the full type would be a cycle) +export interface FunctionTypeMaker { + createFunctionType(ret: llvm.Type, params: llvm.Type[]): llvm.FunctionType; +} + +export const getEjsClosureFunc = (abi: FunctionTypeMaker): llvm.Type => + abi + .createFunctionType(EjsValue, [ + EjsValue, + EjsValue.pointerTo(), + Int32, + EjsValue.pointerTo(), + EjsValue, + ]) + .pointerTo(); + +// {u64 gc_header, u32 length, i32 hash, 8-byte data union} — matches the +// runtime's 24-byte _EJSPrimString; only the size matters here (globals of +// this type are zero-initialized and filled by _ejs_string_init_literal) +export const EjsPrimString = llvm.StructType.create("EjsPrimString", [ + Int32, + Int32, + Int32, + Int32, + Int64, +]); + +export const EjsSpecops = llvm.StructType.create("struct.EJSSpecOps", []); // XXX + +export const EjsPropertyMap = llvm.StructType.create("struct.EJSPropertyMap", [ + JSChar.pointerTo(), // _EJSPropertyMapSlot** slots + JSChar.pointerTo(), // _EJSPropertyMapSlot* first_insert + JSChar.pointerTo(), // _EJSPropertyMapSlot* last_insert + Int32, // int nslots; + Int32, // int inuse; +]); + +// initialized by initTypes() once the target's pointer size is known; +// reading them before that is a bug (they trap as undefined at runtime) +export let EjsObject: llvm.StructType; +export let EjsFunction: llvm.StructType; +export let EjsModule: llvm.StructType; + +function CreateModuleTy(suffix: string, num_exports: number): llvm.StructType { + return llvm.StructType.create(`struct.EJSModule${suffix}`, [ + EjsObject, // EJSObject obj; + String, // const char* module_name + Int32, // int32_t num_exports + llvm.ArrayType.get(EjsValueLayout, num_exports), + ]); +} + +export function getModuleSpecificType(module_name: string, num_exports: number): llvm.StructType { + return CreateModuleTy(`_${module_name}`, num_exports); +} + +export function initTypes(is32bit: boolean): void { + // EJSObject's struct type depends on the pointer size of the + // architecture. on 32 bit platforms (XXX or maybe just x86?) + // clang inserts 4 bytes of padding at the end. we therefore need + // to delay initialization of EJSObject (and therefore its uses) + // until after we've determined pointer size. + + // the 64-bit GCObjectHeader is represented as two i32s (little-endian + // halves) so the shape-guard emitter can load the shape/gc half + // (field 1) without masking a 64-bit load; byte layout is identical + if (is32bit) { + EjsObject = llvm.StructType.create("struct.EJSObject", [ + Int32, // GCObjectHeader gc_header (low half: scan type, user flags) + Int32, // GCObjectHeader shape index / gc bits (high half) + EjsSpecops.pointerTo(), // EJSSpecOps* ops; + EjsValue, // ejsval proto; // the __proto__ property + EjsPropertyMap.pointerTo(), // EJSPropertyMap map; + llvm.ArrayType.get(llvm.Type.getInt8Ty(), 4), // alignment that clang adds + ]); + } else { + EjsObject = llvm.StructType.create("struct.EJSObject", [ + Int32, // GCObjectHeader gc_header (low half: scan type, user flags) + Int32, // GCObjectHeader shape index / gc bits (high half) + EjsSpecops.pointerTo(), // EJSSpecOps* ops; + EjsValue, // ejsval proto; // the __proto__ property + EjsPropertyMap.pointerTo(), // EJSPropertyMap map; + ]); + } + + EjsFunction = llvm.StructType.create("struct.EJSFunction", [ + EjsObject, // EJSObject obj; + EjsClosureFunc, // EJSClosureFunc func; + EjsValue, // ejsval env; + + Int32, // EJSBool bound; + ]); + + EjsModule = CreateModuleTy("", 1); +} + +// exception types + +// the c++ typeinfo for our exceptions +export const EjsExceptionTypeInfo = llvm.StructType.create("EjsExceptionTypeInfoType", [ + Int8Pointer, + Int8Pointer, + Int8Pointer, +]).pointerTo(); + +export function takes_builtins(n: llvm.EjsFunction): llvm.EjsFunction { + n.takes_builtins = true; + return n; +} + +export function only_reads_memory(n: llvm.EjsFunction): llvm.EjsFunction { + n.setOnlyReadsMemory(); + return n; +} + +export function does_not_access_memory(n: llvm.EjsFunction): llvm.EjsFunction { + n.setDoesNotAccessMemory(); + return n; +} + +export function does_not_throw(n: llvm.EjsFunction): llvm.EjsFunction { + n.setDoesNotThrow(); + return n; +} + +export function returns_ejsval_bool(n: llvm.EjsFunction): llvm.EjsFunction { + n.returns_ejsval_bool = true; + return n; +} diff --git a/modules/objc_internal/objc_internal.ejs b/modules/objc_internal/objc_internal.ejs index 1669d4fe..4872f115 100644 --- a/modules/objc_internal/objc_internal.ejs +++ b/modules/objc_internal/objc_internal.ejs @@ -4,17 +4,17 @@ "submodules": [], "init_function": "_ejs_objc_module_func", "exports": [ - "requireFramework", - "allocInstance", - "staticCall", - "getInstanceVariable", - "setInstanceVariable", - "selectorInvoker", - "getTypeEncoding", - "registerJSClass", - "allocateWebGLRenderingContext", - "UIApplicationMain", - "NSApplicationMain" + "requireFramework", + "allocInstance", + "staticCall", + "getInstanceVariable", + "setInstanceVariable", + "selectorInvoker", + "getTypeEncoding", + "registerJSClass", + "allocateWebGLRenderingContext", + "UIApplicationMain", + "NSApplicationMain" ], "link_flags": "", "module_version": "0.1.0-alpha.1" diff --git a/node-compat/BUCK b/node-compat/BUCK new file mode 100644 index 00000000..2f103977 --- /dev/null +++ b/node-compat/BUCK @@ -0,0 +1,17 @@ +load("//:defs.bzl", "EJS_COMPILER_FLAGS") + +export_file( + name = "node-compat.ejs", + visibility = ["PUBLIC"], +) + +cxx_library( + name = "node-compat", + srcs = ["ejs-node-compat.c"], + header_namespace = "", + exported_headers = glob(["*.h"]), + compiler_flags = EJS_COMPILER_FLAGS, + preferred_linkage = "static", + deps = ["//runtime:echo"], + visibility = ["PUBLIC"], +) diff --git a/node-compat/Makefile b/node-compat/Makefile deleted file mode 100644 index cff22771..00000000 --- a/node-compat/Makefile +++ /dev/null @@ -1,127 +0,0 @@ -TOP=.. - -include $(TOP)/build/config.mk - -LIBRARY=libejsnodecompat-module.a -C_SOURCES= \ - ejs-node-compat.c - -CFLAGS += -I../runtime - -ejs-atoms-gen.c: ejs-atoms.h gen-atoms.js - @echo [GEN] $@ && ./gen-atoms.js $< > .tmp-$@ && mv .tmp-$@ $@ - -ifeq ($(HOST_OS),linux) -ALL_LIBRARIES=$(LIBRARY) -ALL_TARGETS=$(ALL_LIBRARIES) - -LINUX_OBJECTS=$(C_SOURCES:%.c=%.o.linux) - -ALL_OBJECTS=$(LINUX_OBJECTS) - -$(LIBRARY): $(LINUX_OBJECTS) - @echo [ar linux] $@ && /usr/bin/ar rc $@ $(LINUX_OBJECTS) - -%.o.linux: %.c - @mkdir -p .deps - @$(CC) -MM $(LINUX_CFLAGS) $< | sed -e s/`echo $@ | sed -e s,.linux,,`/$@/ > .deps/$@-deps - @echo [$(CC) linux] $< && $(CC) $(LINUX_CFLAGS) -c -o $@ $< - --include $(patsubst %.o.linux,.deps/%.o.linux-deps,$(LINUX_OBJECTS)) -endif - -ifeq ($(HOST_OS),darwin) - -OBJC_SOURCES= - -OSX_OBJECTS=$(C_SOURCES:%.c=%.o.osx) $(OBJC_SOURCES:%.m=%.o.osx) -SIM_OBJECTS=$(C_SOURCES:%.c=%.o.sim) $(OBJC_SOURCES:%.m=%.o.sim) -DEV_OBJECTS=$(C_SOURCES:%.c=%.o.armv7) $(OBJC_SOURCES:%.m=%.o.armv7) -DEVS_OBJECTS=$(C_SOURCES:%.c=%.o.armv7s) $(OBJC_SOURCES:%.m=%.o.armv7s) - -analyze_plists_c = $(C_SOURCES:%.c=%.plist) -analyze_plists_objc = $(OBJC_SOURCES:%.m=%.plist) - -OSX_LIBRARY=$(LIBRARY) -SIM_LIBRARY=$(LIBRARY).sim -DEV_LIBRARY=$(LIBRARY).armv7 -DEVS_LIBRARY=$(LIBRARY).armv7s - -ifneq ($(CIRCLE_BUILD_NUM),) -# on circleci we only build the osx library -ALL_LIBRARIES=$(OSX_LIBRARY) -ALL_TARGETS=$(ALL_LIBRARIES) -else -# on local builds we build all the libraries (XXX need to figure out how to accurately target those platforms first) -ALL_LIBRARIES=$(OSX_LIBRARY) $(SIM_LIBRARY) $(DEV_LIBRARY) $(DEVS_LIBRARY) -ALL_TARGETS=$(ALL_LIBRARIES) $(analyze_plists_c) $(analyze_plists_objc) -endif - -ALL_OBJECTS=$(SIM_OBJECTS) $(DEV_OBJECTS) $(DEVS_OBJECTS) $(OSX_OBJECTS) - -$(OSX_LIBRARY): $(OSX_OBJECTS) - @echo [ar osx] $@ && /usr/bin/ar rc $@ $(OSX_OBJECTS) - -$(SIM_LIBRARY): $(SIM_OBJECTS) - @echo [ar sim] $@ && /usr/bin/ar rc $@ $(SIM_OBJECTS) - -$(DEV_LIBRARY): $(DEV_OBJECTS) - @echo [ar armv7] $@ && /usr/bin/ar rc $@ $(DEV_OBJECTS) - -$(DEVS_LIBRARY): $(DEVS_OBJECTS) - @echo [ar armv7s] $@ && /usr/bin/ar rc $@ $(DEVS_OBJECTS) - -%.o.osx: %.c - @mkdir -p .deps - @$(CC) -MM $(OSX_CFLAGS) $< | sed -e s/`echo $@ | sed -e s,.osx,,`/$@/ > .deps/$@-deps - @echo [$(CC) osx] $< && $(CC) -ObjC $(OSX_CFLAGS) -c -o $@ $< - -%.o.osx: %.ll - @echo [llc osx] $< && llc$(LLVM_SUFFIX) -filetype=obj -o=$@ -O2 $< - -%.o.sim: %.c - @mkdir -p .deps - @$(CC) -MM $(IOSSIM_CFLAGS) $< | sed -e s/`echo $@ | sed -e s,.sim,,`/$@/ > .deps/$@-deps - @echo [$(CC) sim] $< && PATH=$(IOSSIM_BIN):$$PATH $(CC) -ObjC $(IOSSIM_CFLAGS) -c -o $@ $< - -%.o.sim: %.ll - @echo [llc sim] $< && llc$(LLVM_SUFFIX) -march=x86 -filetype=obj -o=$@ -O2 $< - -%.o.armv7: %.c - @mkdir -p .deps - @$(CC) -MM -ObjC $(IOSDEV_CFLAGS) $< | sed -e s/`echo $@ | sed -e s,.armv7,,`/$@/ > .deps/$@-deps - @echo [$(CC) armv7] $< && PATH=$(IOSDEV_BIN):$$PATH $(CC) -ObjC $(IOSDEV_CFLAGS) -c -o $@ $< - -%.o.armv7: %.ll - @echo [llc armv7] $< && llc$(LLVM_SUFFIX) -march=arm -filetype=obj -o=$@ -O2 $< - -%.o.armv7s: %.c - @mkdir -p .deps - @$(CC) -MM -ObjC $(IOSDEVS_CFLAGS) $< | sed -e s/`echo $@ | sed -e s,.armv7s,,`/$@/ > .deps/$@-deps - @echo [$(CC) armv7s] $< && PATH=$(IOSDEVS_BIN):$$PATH $(CC) -ObjC $(IOSDEVS_CFLAGS) -c -o $@ $< - -%.o.armv7s: %.ll - @echo [llc armv7s] $< && llc$(LLVM_SUFFIX) -march=aarch64 -filetype=obj -o=$@ -O2 $< - -$(analyze_plists_c): %.plist: %.c - @echo [$(CC) analyze] $< && PATH=$(IOSDEVS_BIN):$$PATH $(CC) $(OSX_CFLAGS) --analyze $< -o $@ - --include $(patsubst %.o.osx,.deps/%.o.osx-deps,$(OSX_OBJECTS)) --include $(patsubst %.o.sim,.deps/%.o.sim-deps,$(SIM_OBJECTS)) --include $(patsubst %.o.armv7,.deps/%.o.armv7-deps,$(DEV_OBJECTS)) --include $(patsubst %.o.armv7s,.deps/%.o.armv7s-deps,$(DEVS_OBJECTS)) -endif - -all-local:: $(ALL_TARGETS) - -clean-local:: - rm -f test $(ALL_OBJECTS) $(ALL_LIBRARIES) ejs-atoms-gen.c $(analyze_plists_c) $(analyze_plists_objc) - -#XXX(toshok) this doesn't work for osx where we want to install multiple libraries to different archlibdirs... -install-local:: - @$(MKDIR) $(libdir) - @$(MKDIR) $(archlibdir) - $(INSTALL) -c node-compat.ejs $(libdir) - $(INSTALL) -c $(LIBRARY) $(archlibdir) - -include $(TOP)/build/build.mk diff --git a/node-compat/ejs-node-compat.c b/node-compat/ejs-node-compat.c index 77a06e74..85d10241 100644 --- a/node-compat/ejs-node-compat.c +++ b/node-compat/ejs-node-compat.c @@ -743,16 +743,19 @@ static EJS_NATIVE_FUNC(_ejs_child_process_spawn) { for (uint32_t i = 0; i < EJSARRAY_LEN(argv_rest); i ++) argv[1+i] = ucs2_to_utf8(EJSVAL_TO_FLAT_STRING(ToString(EJSDENSEARRAY_ELEMENTS(argv_rest)[i]))); + // synchronous: returns the child's exit status (127 = exec failed, + // 128+signal for signal deaths, -1 = fork/waitpid failure) so callers + // can stop the build instead of silently continuing past a failed tool + int exit_status = -1; pid_t pid; switch (pid = fork()) { case -1: /* error */ perror("fork"); - printf ("we should totally throw an exception here\n"); break; case 0: /* child */ execvp (argv0, argv); - perror("execv"); - EJS_NOT_REACHED(); + perror(argv0); + _exit(127); break; default: /* parent */ { int stat; @@ -761,17 +764,19 @@ static EJS_NATIVE_FUNC(_ejs_child_process_spawn) { wait_rv = waitpid(pid, &stat, 0); } while (wait_rv == -1 && errno == EINTR); - if (wait_rv != pid) { + if (wait_rv != pid) perror ("waitpid"); - printf ("we should totally throw an exception here\n"); - } + else if (WIFEXITED(stat)) + exit_status = WEXITSTATUS(stat); + else if (WIFSIGNALED(stat)) + exit_status = 128 + WTERMSIG(stat); break; } } for (uint32_t i = 0; i < EJSARRAY_LEN(argv_rest)+1; i ++) free (argv[i]); free (argv); - return _ejs_undefined; + return NUMBER_TO_EJSVAL(exit_status); } ejsval diff --git a/node-compat/node-compat.ejs b/node-compat/node-compat.ejs index 7951380e..af07518b 100644 --- a/node-compat/node-compat.ejs +++ b/node-compat/node-compat.ejs @@ -1,20 +1,55 @@ { "ejs_version": "0.1.0-alpha.3", - "module_name": "node-compat", "submodules": [ - { "module_name": "path", "init_function": "_ejs_path_module_func", "exports": [ "dirname", "basename", "extname", "resolve", "relative", "join" ] }, - { "module_name": "os", "init_function": "_ejs_os_module_func", "exports": [ "arch", "platform", "tmpdir" ] }, - { "module_name": "fs", "init_function": "_ejs_fs_module_func", "exports": [ "statSync", "readFileSync", "createWriteStream", "readdirSync" ] }, - { "module_name": "child_process", "init_function": "_ejs_child_process_module_func", "exports": [ "spawn", "stdout", "stderr" ] } + { + "module_name": "path", + "init_function": "_ejs_path_module_func", + "exports": [ + "dirname", + "basename", + "extname", + "resolve", + "relative", + "join" + ] + }, + { + "module_name": "os", + "init_function": "_ejs_os_module_func", + "exports": [ + "arch", + "platform", + "tmpdir" + ] + }, + { + "module_name": "fs", + "init_function": "_ejs_fs_module_func", + "exports": [ + "statSync", + "readFileSync", + "createWriteStream", + "readdirSync" + ] + }, + { + "module_name": "child_process", + "init_function": "_ejs_child_process_module_func", + "exports": [ + "spawn", + "stdout", + "stderr" + ] + } ], "link_flags": "", "module_version": "0.1.0-alpha.2", "module_file": { - "darwin-arm64": "libejsnodecompat-module.a", - "darwin-x86_64": "libejsnodecompat-module.a", - "linux-x86_64": "libejsnodecompat-module.a", - "darwin-x86": "libejsnodecompat-module.a.sim", - "darwin-armv7": "libejsnodecompat-module.a.armv7" + "arm64-linux": "libejsnodecompat-module.a", + "x86_64-linux": "libejsnodecompat-module.a", + "arm64-macos": "libejsnodecompat-module.a", + "arm64-ios-simulator": "libejsnodecompat-module.a.iossim", + "arm64-ios": "libejsnodecompat-module.a.iosdev" } } diff --git a/node-llvm/BUCK b/node-llvm/BUCK new file mode 100644 index 00000000..68076828 --- /dev/null +++ b/node-llvm/BUCK @@ -0,0 +1,10 @@ +# The node native addon that gives the node-hosted (stage0) compiler access +# to LLVM. Built out-of-band with node-gyp (`./build-addon.sh`); buck just +# picks up the built addon. +# +# TODO(buck2): drive node-gyp from a genrule so this is built hermetically. +export_file( + name = "llvm.node", + src = "build/Release/llvm.node", + visibility = ["PUBLIC"], +) diff --git a/node-llvm/Makefile b/node-llvm/Makefile deleted file mode 100644 index 6827b232..00000000 --- a/node-llvm/Makefile +++ /dev/null @@ -1,33 +0,0 @@ -TOP=.. - --include $(TOP)/build/config.mk - -LLVM_CONFIG=llvm-config$(LLVM_SUFFIX) - -LLVM_CXXFLAGS := $(shell $(LLVM_CONFIG) --cxxflags) -LLVM_CPPFLAGS := $(shell $(LLVM_CONFIG) --cppflags) -LLVM_INCLUDEDIR := $(shell $(LLVM_CONFIG) --includedir) - -LLVM_CXXFLAGS := $(subst $(LLVM_CPPFLAGS),,$(LLVM_CXXFLAGS)) -LLVM_DEFINES := $(subst -I$(LLVM_INCLUDEDIR),,$(LLVM_CPPFLAGS)) -LLVM_DEFINES := $(subst -D,,$(LLVM_DEFINES)) - -LLVM_LINKFLAGS := $(shell $(LLVM_CONFIG) --ldflags --libs) - -MIN_OSX_VERSION?=10.9 - -all-local:: build - -build: configure - @CC="$(CC)" CXX="$(CXX)" LLVM_CXXFLAGS="$(LLVM_CXXFLAGS)" LLVM_INCLUDEDIR="$(LLVM_INCLUDEDIR)" LLVM_LINKFLAGS="$(LLVM_LINKFLAGS)" LLVM_DEFINES="$(LLVM_DEFINES)" MIN_OSX_VERSION="$(MIN_OSX_VERSION)" node-gyp build - -configure: - @echo "LLVM LINKFLAGS == $(LLVM_LINKFLAGS)" - @$(CC) --version - @$(CXX) --version - @CC="$(CC)" CXX="$(CXX)" LLVM_CXXFLAGS="$(LLVM_CXXFLAGS)" LLVM_INCLUDEDIR="$(LLVM_INCLUDEDIR)" LLVM_LINKFLAGS="$(LLVM_LINKFLAGS)" LLVM_DEFINES="$(LLVM_DEFINES)" MIN_OSX_VERSION="$(MIN_OSX_VERSION)" node-gyp configure - -clean-local:: - node-gyp clean - --include $(TOP)/build/build.mk diff --git a/node-llvm/build-addon.sh b/node-llvm/build-addon.sh new file mode 100755 index 00000000..fb35b315 --- /dev/null +++ b/node-llvm/build-addon.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# Builds the node-llvm addon (build/Release/llvm.node), which the stage0 +# (node-hosted) compiler uses to drive llvm. //node-llvm:llvm.node picks +# up the built addon; run this after changing node-llvm sources or +# switching llvm versions. +# +# usage: ./build-addon.sh [llvm-prefix] (default: /opt/homebrew/opt/llvm) +set -euo pipefail +cd "$(dirname "$0")" + +LLVM_PREFIX="${1:-/opt/homebrew/opt/llvm}" +LLVM_CONFIG="$LLVM_PREFIX/bin/llvm-config" + +export PATH="$LLVM_PREFIX/bin:$PATH" + +LLVM_CXXFLAGS="$($LLVM_CONFIG --cxxflags) -fno-rtti" \ +LLVM_INCLUDEDIR="$($LLVM_CONFIG --includedir)" \ +LLVM_DEFINES="" \ +LLVM_LINKFLAGS="$($LLVM_CONFIG --ldflags --libs)" \ +MIN_OSX_VERSION=11.0 \ + npx -y node-gyp@10 rebuild + +node -e "require('./build/Release/llvm.node'); console.log('llvm.node loads ok')" diff --git a/node-llvm/constant.cpp b/node-llvm/constant.cpp index ebb4df68..d5082575 100644 --- a/node-llvm/constant.cpp +++ b/node-llvm/constant.cpp @@ -68,13 +68,17 @@ namespace jsllvm { Local result; if (info.Length() == 2) { - result = Value::Create(llvm::Constant::getIntegerValue(ty, llvm::APInt(ty->getPrimitiveSizeInBits(), v))); + // llvm 20+ asserts on implicit truncation; keep the old truncating + // behavior for negative/oversized js numbers + result = Value::Create(llvm::Constant::getIntegerValue(ty, llvm::APInt(ty->getPrimitiveSizeInBits(), v, /*isSigned*/ true, /*implicitTrunc*/ true))); } else if (info.Length() == 3 && info[2]->IsNumber() && ty->getPrimitiveSizeInBits() == 64) { // allow a 3 arg form for 64 bit ints: // constant = llvm.Constant.getIntegerValue types.int64, ch, cl uint64_t vhi = v; - uint32_t vlo = (uint32_t)info[2]->NumberValue(context).ToChecked(); + // convert with ToUint32 (wrapping) semantics: a bare double->uint32_t + // cast of a negative value saturates to 0 on arm64 + uint32_t vlo = (uint32_t)(int64_t)info[2]->NumberValue(context).ToChecked(); result = Value::Create (llvm::Constant::getIntegerValue(ty, llvm::APInt(ty->getPrimitiveSizeInBits(), (int64_t)((vhi << 32) | vlo)))); } else { diff --git a/node-llvm/irbuilder.cpp b/node-llvm/irbuilder.cpp index dbdfaa58..32c04f60 100644 --- a/node-llvm/irbuilder.cpp +++ b/node-llvm/irbuilder.cpp @@ -6,6 +6,7 @@ #include "value.h" #include "instruction.h" #include "landingpad.h" +#include "phinode.h" #include "switch.h" #include "callinvoke.h" #include "basicblock.h" @@ -41,6 +42,10 @@ namespace jsllvm { Nan::SetMethod(ctor_func, "createCall", IRBuilder::CreateCall); Nan::SetMethod(ctor_func, "createInvoke", IRBuilder::CreateInvoke); Nan::SetMethod(ctor_func, "createFAdd", IRBuilder::CreateFAdd); + Nan::SetMethod(ctor_func, "createFSub", IRBuilder::CreateFSub); + Nan::SetMethod(ctor_func, "createFMul", IRBuilder::CreateFMul); + Nan::SetMethod(ctor_func, "createFDiv", IRBuilder::CreateFDiv); + Nan::SetMethod(ctor_func, "createFCmpOLT", IRBuilder::CreateFCmpOLT); Nan::SetMethod(ctor_func, "createAlloca", IRBuilder::CreateAlloca); Nan::SetMethod(ctor_func, "createLoad", IRBuilder::CreateLoad); Nan::SetMethod(ctor_func, "createStore", IRBuilder::CreateStore); @@ -318,6 +323,58 @@ namespace jsllvm { info.GetReturnValue().Set(result); } + NAN_METHOD(IRBuilder::CreateFSub) { + v8::Isolate *isolate = info.GetIsolate(); + v8::Local context = isolate->GetCurrentContext(); + Nan::HandleScope scope; + + REQ_LLVM_VAL_ARG(context, 0, left); + REQ_LLVM_VAL_ARG(context, 1, right); + FALLBACK_EMPTY_UTF8_ARG(context, 2, name); + + Local result = Instruction::Create(static_cast(IRBuilder::builder.CreateFSub(left, right, *name))); + info.GetReturnValue().Set(result); + } + + NAN_METHOD(IRBuilder::CreateFMul) { + v8::Isolate *isolate = info.GetIsolate(); + v8::Local context = isolate->GetCurrentContext(); + Nan::HandleScope scope; + + REQ_LLVM_VAL_ARG(context, 0, left); + REQ_LLVM_VAL_ARG(context, 1, right); + FALLBACK_EMPTY_UTF8_ARG(context, 2, name); + + Local result = Instruction::Create(static_cast(IRBuilder::builder.CreateFMul(left, right, *name))); + info.GetReturnValue().Set(result); + } + + NAN_METHOD(IRBuilder::CreateFDiv) { + v8::Isolate *isolate = info.GetIsolate(); + v8::Local context = isolate->GetCurrentContext(); + Nan::HandleScope scope; + + REQ_LLVM_VAL_ARG(context, 0, left); + REQ_LLVM_VAL_ARG(context, 1, right); + FALLBACK_EMPTY_UTF8_ARG(context, 2, name); + + Local result = Instruction::Create(static_cast(IRBuilder::builder.CreateFDiv(left, right, *name))); + info.GetReturnValue().Set(result); + } + + NAN_METHOD(IRBuilder::CreateFCmpOLT) { + v8::Isolate *isolate = info.GetIsolate(); + v8::Local context = isolate->GetCurrentContext(); + Nan::HandleScope scope; + + REQ_LLVM_VAL_ARG(context, 0, left); + REQ_LLVM_VAL_ARG(context, 1, right); + FALLBACK_EMPTY_UTF8_ARG(context, 2, name); + + Local result = Instruction::Create(static_cast(IRBuilder::builder.CreateFCmpOLT(left, right, *name))); + info.GetReturnValue().Set(result); + } + NAN_METHOD(IRBuilder::CreateAlloca) { v8::Isolate *isolate = info.GetIsolate(); v8::Local context = isolate->GetCurrentContext(); @@ -540,7 +597,9 @@ namespace jsllvm { REQ_INT_ARG(context, 1, incoming_values); FALLBACK_EMPTY_UTF8_ARG(context, 2, name); - Local result = Instruction::Create(static_cast(IRBuilder::builder.CreatePHI(ty, incoming_values, *name))); + // return the PHINode wrapper (not the generic Instruction one) so + // callers can use addIncoming + Local result = PHINode::Create(IRBuilder::builder.CreatePHI(ty, incoming_values, *name)); info.GetReturnValue().Set(result); } @@ -552,7 +611,9 @@ namespace jsllvm { FALLBACK_EMPTY_UTF8_ARG(context, 0, val); FALLBACK_EMPTY_UTF8_ARG(context, 1, name); - Local result = Constant::Create(IRBuilder::builder.CreateGlobalStringPtr(*val, *name)); + // CreateGlobalStringPtr was removed in llvm 20; CreateGlobalString is + // identical under opaque pointers + Local result = Constant::Create(IRBuilder::builder.CreateGlobalString(*val, *name)); info.GetReturnValue().Set(result); } @@ -631,7 +692,9 @@ namespace jsllvm { REQ_LLVM_VAL_ARG(context, 0, val); REQ_LLVM_CONST_INT_ARG(context, 1, size); - Local result = Instruction::Create(static_cast(IRBuilder::builder.CreateLifetimeStart(val, size))); + // llvm 22 lifetime intrinsics are size-less; the size arg is ignored + (void)size; + Local result = Instruction::Create(static_cast(IRBuilder::builder.CreateLifetimeStart(val))); info.GetReturnValue().Set(result); } @@ -643,7 +706,9 @@ namespace jsllvm { REQ_LLVM_VAL_ARG(context, 0, val); REQ_LLVM_CONST_INT_ARG(context, 1, size); - Local result = Instruction::Create(static_cast(IRBuilder::builder.CreateLifetimeEnd(val, size))); + // llvm 22 lifetime intrinsics are size-less; the size arg is ignored + (void)size; + Local result = Instruction::Create(static_cast(IRBuilder::builder.CreateLifetimeEnd(val))); info.GetReturnValue().Set(result); } diff --git a/node-llvm/irbuilder.h b/node-llvm/irbuilder.h index 1ed8b3a4..91833100 100644 --- a/node-llvm/irbuilder.h +++ b/node-llvm/irbuilder.h @@ -25,6 +25,10 @@ namespace jsllvm { static NAN_METHOD(CreateCall); static NAN_METHOD(CreateInvoke); static NAN_METHOD(CreateFAdd); + static NAN_METHOD(CreateFSub); + static NAN_METHOD(CreateFMul); + static NAN_METHOD(CreateFDiv); + static NAN_METHOD(CreateFCmpOLT); static NAN_METHOD(CreateAlloca); static NAN_METHOD(CreateLoad); static NAN_METHOD(CreateStore); diff --git a/node-llvm/module.cpp b/node-llvm/module.cpp index e3b7d624..8ea1210b 100644 --- a/node-llvm/module.cpp +++ b/node-llvm/module.cpp @@ -84,9 +84,10 @@ namespace jsllvm { } #if false - llvm::Function* f = llvm::Intrinsic::getDeclaration (module->llvm_obj, intrinsic_id, param_types); + llvm::Function* f = llvm::Intrinsic::getOrInsertDeclaration (module->llvm_obj, intrinsic_id, param_types); #else - llvm::Function* f = llvm::Intrinsic::getDeclaration (module->llvm_obj, intrinsic_id); + // renamed from getDeclaration in llvm 20 + llvm::Function* f = llvm::Intrinsic::getOrInsertDeclaration (module->llvm_obj, intrinsic_id); #endif Local result = Function::Create(f); @@ -249,7 +250,8 @@ namespace jsllvm { REQ_UTF8_ARG(context, 0, triple); - module->llvm_obj->setTargetTriple (*triple); + // setTargetTriple takes an llvm::Triple as of llvm 21 + module->llvm_obj->setTargetTriple (llvm::Triple(*triple)); } Nan::Persistent Module::constructor; diff --git a/node-llvm/phinode.h b/node-llvm/phinode.h index a73f0995..7a05a524 100644 --- a/node-llvm/phinode.h +++ b/node-llvm/phinode.h @@ -8,7 +8,10 @@ namespace jsllvm { public: static NAN_MODULE_INIT(Init); - static v8::Local Create(::llvm::PHINode *llvm_phi); + // the base template's Create() is what we want; redeclaring it here + // (without a definition) shadowed it, and -undefined dynamic_lookup + // deferred the missing symbol to a null pointer at runtime. + using LLVMObjectWrap< ::llvm::PHINode, PHINode>::Create; private: typedef LLVMObjectWrap< ::llvm::PHINode, PHINode> BaseType; diff --git a/node-llvm/type.cpp b/node-llvm/type.cpp index be33b2e4..e182ae5b 100644 --- a/node-llvm/type.cpp +++ b/node-llvm/type.cpp @@ -66,7 +66,8 @@ namespace jsllvm { NAN_METHOD(Type::pointerTo) { auto type = Unwrap(info.This()); - info.GetReturnValue().Set(Type::Create(type->llvm_obj->getPointerTo())); + // Type::getPointerTo was removed in llvm 21; all pointers are opaque + info.GetReturnValue().Set(Type::Create(llvm::PointerType::getUnqual(type->llvm_obj->getContext()))); } NAN_METHOD(Type::isVoid) { diff --git a/package-lock.json b/package-lock.json index 04cbaeb6..919a23df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6007 +1,1766 @@ { "name": "echojs", - "version": "0.0.0", + "version": "0.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "echojs", - "version": "0.0.0", + "version": "0.2.0", "license": "MIT", "dependencies": { - "@babel/cli": "^7.22.15", - "@babel/node": "^7.22.19", - "@babel/preset-env": "^7.22.20", "colors": "^1.4.0", "glob": "^10.3.4", "mocha": "^10.2.0", - "nan": "^2.18.0", "prettier": "^3.0.3", "temp": "^0.9.4" }, "bin": { "ejs": "ejs-driver.js" + }, + "devDependencies": { + "@types/node": "^26.1.1", + "nan": "^2.28.0", + "typescript": "^7.0.2" } }, - "node_modules/@ampproject/remapping": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.1.2.tgz", - "integrity": "sha512-hoyByceqwKirw7w3Z7gnIIZC3Wx3J484Y3L/cMpXFbr7d9ZQj2mODrirNzcJa+SM3UlpWXYvKV4RlRpFXlWgXg==", - "peer": true, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.0" + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" }, "engines": { - "node": ">=6.0.0" + "node": ">=12" } }, - "node_modules/@babel/cli": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.22.15.tgz", - "integrity": "sha512-prtg5f6zCERIaECeTZzd2fMtVjlfjhUcO+fBLQ6DXXdq5FljN+excVitJ2nogsusdf31LeqkjAfXZ7Xq+HmN8g==", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.17", - "commander": "^4.0.1", - "convert-source-map": "^1.1.0", - "fs-readdir-recursive": "^1.1.0", - "glob": "^7.2.0", - "make-dir": "^2.1.0", - "slash": "^2.0.0" + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "engines": { + "node": ">=12" }, - "bin": { - "babel": "bin/babel.js", - "babel-external-helpers": "bin/babel-external-helpers.js" + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" }, "engines": { - "node": ">=6.9.0" - }, - "optionalDependencies": { - "@nicolo-ribaudo/chokidar-2": "2.1.8-no-fsevents.3", - "chokidar": "^3.4.0" + "node": ">=12" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@babel/cli/node_modules/anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - "optional": true, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" }, "engines": { - "node": ">= 8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/@babel/cli/node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "optional": true, "engines": { - "node": ">=8" + "node": ">=14" } }, - "node_modules/@babel/cli/node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "optional": true, + "node_modules/@types/node": { + "version": "26.1.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", + "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", + "dev": true, + "license": "MIT", "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" + "undici-types": "~8.3.0" } }, - "node_modules/@babel/cli/node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" ], + "dev": true, + "license": "Apache-2.0", "optional": true, - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, + "os": [ + "aix" + ], "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" + "node": ">=16.20.0" } }, - "node_modules/@babel/cli/node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", "optional": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, + "os": [ + "darwin" + ], "engines": { - "node": ">=8" + "node": ">=16.20.0" } }, - "node_modules/@babel/cli/node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "hasInstallScript": true, + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", "optional": true, "os": [ "darwin" ], "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">=16.20.0" } }, - "node_modules/@babel/cli/node_modules/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=16.20.0" } }, - "node_modules/@babel/cli/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", "optional": true, - "dependencies": { - "is-glob": "^4.0.1" - }, + "os": [ + "freebsd" + ], "engines": { - "node": ">= 6" + "node": ">=16.20.0" } }, - "node_modules/@babel/cli/node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", "optional": true, - "dependencies": { - "binary-extensions": "^2.0.0" - }, + "os": [ + "linux" + ], "engines": { - "node": ">=8" + "node": ">=16.20.0" } }, - "node_modules/@babel/cli/node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=0.10.0" + "node": ">=16.20.0" } }, - "node_modules/@babel/cli/node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "Apache-2.0", "optional": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, + "os": [ + "linux" + ], "engines": { - "node": ">=0.10.0" + "node": ">=16.20.0" } }, - "node_modules/@babel/cli/node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "Apache-2.0", "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=0.10.0" + "node": ">=16.20.0" } }, - "node_modules/@babel/cli/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", "optional": true, - "dependencies": { - "picomatch": "^2.2.1" - }, + "os": [ + "linux" + ], "engines": { - "node": ">=8.10.0" + "node": ">=16.20.0" } }, - "node_modules/@babel/cli/node_modules/slash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6" + "node": ">=16.20.0" } }, - "node_modules/@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", - "dependencies": { - "@babel/highlight": "^7.22.13", - "chalk": "^2.4.2" - }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6.9.0" + "node": ">=16.20.0" } }, - "node_modules/@babel/compat-data": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.20.tgz", - "integrity": "sha512-BQYjKbpXjoXwFW5jGqiizJQQT/aC7pFm9Ok1OWssonuguICi264lbgMzRp2ZMmRSlfkX6DsWDDcsrctK8Rwfiw==", + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6.9.0" + "node": ">=16.20.0" } }, - "node_modules/@babel/core": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.17.9.tgz", - "integrity": "sha512-5ug+SfZCpDAkVp9SFIZAzlW18rlzsOcJGaetCjkySnrXXDUw9AR8cDUm1iByTmdWM6yxX6/zycaV76w3YTF2gw==", - "peer": true, - "dependencies": { - "@ampproject/remapping": "^2.1.0", - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.9", - "@babel/helper-compilation-targets": "^7.17.7", - "@babel/helper-module-transforms": "^7.17.7", - "@babel/helpers": "^7.17.9", - "@babel/parser": "^7.17.9", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.9", - "@babel/types": "^7.17.0", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.1", - "semver": "^6.3.0" - }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" + "node": ">=16.20.0" } }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "peer": true, - "bin": { - "semver": "bin/semver.js" + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" } }, - "node_modules/@babel/generator": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.9.tgz", - "integrity": "sha512-rAdDousTwxbIxbz5I7GEQ3lUip+xVCXooZNbsydCWs3xA7ZsYOv+CFRdzGxRX78BmQHu9B1Eso59AOZQOJDEdQ==", - "peer": true, - "dependencies": { - "@babel/types": "^7.17.0", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=6.9.0" + "node": ">=16.20.0" } }, - "node_modules/@babel/generator/node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "peer": true, - "bin": { - "jsesc": "bin/jsesc" - }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=4" + "node": ">=16.20.0" } }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", - "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", - "dependencies": { - "@babel/types": "^7.22.5" - }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], "engines": { - "node": ">=6.9.0" + "node": ">=16.20.0" } }, - "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz", - "integrity": "sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==", - "dependencies": { - "@babel/types": "^7.22.15" - }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=6.9.0" + "node": ">=16.20.0" } }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", - "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", - "dependencies": { - "@babel/compat-data": "^7.22.9", - "@babel/helper-validator-option": "^7.22.15", - "browserslist": "^4.21.9", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=6.9.0" + "node": ">=16.20.0" } }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz", - "integrity": "sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-member-expression-to-functions": "^7.22.15", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "semver": "^6.3.1" - }, + "node_modules/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" + "node": ">=6" } }, - "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz", - "integrity": "sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "regexpu-core": "^5.3.1", - "semver": "^6.3.1" - }, + "node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", "engines": { - "node": ">=6.9.0" + "node": ">=12" }, - "peerDependencies": { - "@babel/core": "^7.0.0" + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" - } + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, - "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.2.tgz", - "integrity": "sha512-k0qnnOqHn5dK9pZpfD5XXZ9SojAITdCKRn2Lp6rnDGzIbaP0rHyMPk/4wsSxVBVz4RfN0q6VpXWP2pDGIoQ7hw==", + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dependencies": { - "@babel/helper-compilation-targets": "^7.22.6", - "@babel/helper-plugin-utils": "^7.22.5", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", - "engines": { - "node": ">=6.9.0" - } + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==" }, - "node_modules/@babel/helper-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", - "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", - "dependencies": { - "@babel/template": "^7.22.5", - "@babel/types": "^7.22.5" - }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", - "dependencies": { - "@babel/types": "^7.22.5" + "node": ">=10" }, - "engines": { - "node": ">=6.9.0" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.15.tgz", - "integrity": "sha512-qLNsZbgrNh0fDQBCPocSL8guki1hcPvltGDv/NxvUoABwFq7GkKSu1nRXeJkVZc+wJvne2E0RKQz+2SQrz6eAA==", + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "dependencies": { - "@babel/types": "^7.22.15" - }, - "engines": { - "node": ">=6.9.0" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" } }, - "node_modules/@babel/helper-module-imports": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", - "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", - "dependencies": { - "@babel/types": "^7.22.15" - }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "engines": { - "node": ">=6.9.0" + "node": ">=8" } }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.20.tgz", - "integrity": "sha512-dLT7JVWIUUxKOs1UnJUBR3S70YK+pKX6AbJgB2vMIvEkZkrfJDbYDJesnPshtKV4LhDOR3Oc5YULeDizRek+5A==", + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^7.22.20" + "ansi-regex": "^5.0.1" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" + "node": ">=8" } }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", - "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dependencies": { - "@babel/types": "^7.22.5" + "color-name": "~1.1.4" }, "engines": { - "node": ">=6.9.0" + "node": ">=7.0.0" } }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", - "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", "engines": { - "node": ">=6.9.0" + "node": ">=0.1.90" } }, - "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz", - "integrity": "sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==", + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-wrap-function": "^7.22.20" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" + "node": ">= 8" } }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz", - "integrity": "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==", + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-member-expression-to-functions": "^7.22.15", - "@babel/helper-optimise-call-expression": "^7.22.5" + "ms": "2.1.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=6.0" }, - "peerDependencies": { - "@babel/core": "^7.0.0" + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/@babel/helper-simple-access": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", - "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", - "dependencies": { - "@babel/types": "^7.22.5" - }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", - "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", - "dependencies": { - "@babel/types": "^7.22.5" + "node": ">=10" }, - "engines": { - "node": ">=6.9.0" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", - "dependencies": { - "@babel/types": "^7.22.5" - }, + "node_modules/diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", "engines": { - "node": ">=6.9.0" + "node": ">=0.3.1" } }, - "node_modules/@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", - "engines": { - "node": ">=6.9.0" - } + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", - "engines": { - "node": ">=6.9.0" - } + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, - "node_modules/@babel/helper-validator-option": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", - "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", "engines": { - "node": ">=6.9.0" + "node": ">=6" } }, - "node_modules/@babel/helper-wrap-function": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.20.tgz", - "integrity": "sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==", + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dependencies": { - "@babel/helper-function-name": "^7.22.5", - "@babel/template": "^7.22.15", - "@babel/types": "^7.22.19" + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" }, "engines": { - "node": ">=6.9.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "bin": { + "flat": "cli.js" } }, - "node_modules/@babel/helpers": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.17.9.tgz", - "integrity": "sha512-cPCt915ShDWUEzEp3+UNRktO2n6v49l5RSnG9M5pS24hA+2FAc5si+Pn1i4VVbQQ+jh+bIZhPFQOJOzbrOYY1Q==", - "peer": true, + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", "dependencies": { - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.9", - "@babel/types": "^7.17.0" + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" }, "engines": { - "node": ">=6.9.0" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@babel/highlight": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", - "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", - "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" - }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "engines": { - "node": ">=6.9.0" + "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/@babel/node": { - "version": "7.22.19", - "resolved": "https://registry.npmjs.org/@babel/node/-/node-7.22.19.tgz", - "integrity": "sha512-VsKSO9aEHdO16NdtqkJfrXZ9Sxlna1BVnBbToWr1KGdI3cyIk6KqOoa8mWvpK280lJDOwJqxvnl994KmLhq1Yw==", + "node_modules/glob": { + "version": "10.3.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.4.tgz", + "integrity": "sha512-6LFElP3A+i/Q8XQKEvZjkEWEOTgAIALR9AO2rwT8bgPhDd1anmqDJDZ6lLddI4ehxxxR1S5RIqKe1uapMQfYaQ==", "dependencies": { - "@babel/register": "^7.22.15", - "commander": "^4.0.1", - "core-js": "^3.30.2", - "node-environment-flags": "^1.0.5", - "regenerator-runtime": "^0.14.0", - "v8flags": "^3.1.1" + "foreground-child": "^3.1.0", + "jackspeak": "^2.0.3", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" }, "bin": { - "babel-node": "bin/babel-node.js" + "glob": "dist/cjs/src/bin.js" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/parser": { - "version": "7.22.16", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.16.tgz", - "integrity": "sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA==", - "bin": { - "parser": "bin/babel-parser.js" + "node": ">=16 || 14 >=14.17" }, - "engines": { - "node": ">=6.0.0" + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.15.tgz", - "integrity": "sha512-FB9iYlz7rURmRJyXRKEnalYPPdn87H5no108cyuQQyMwlpJ2SJtpIUBI27kdTin956pz+LPypkPVPUTlxOmrsg==", + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" + "balanced-match": "^1.0.0" } }, - "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.15.tgz", - "integrity": "sha512-Hyph9LseGvAeeXzikV88bczhsrLrIZqDPxO+sSmAunMPaGrBGhfMWzCPYTtiW9t+HzSE2wtV8e5cc5P6r1xMDQ==", + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.22.15" + "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=6.9.0" + "node": ">=16 || 14 >=14.17" }, - "peerDependencies": { - "@babel/core": "^7.13.0" + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0-placeholder-for-preset-env.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", - "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=8" } }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "bin": { + "he": "bin/he" } }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "once": "^1.3.0", + "wrappy": "1" } }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", - "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-export-namespace-from": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", - "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } + "node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, - "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz", - "integrity": "sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=8" } }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz", - "integrity": "sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=8" } }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "engines": { + "node": ">=10" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "node_modules/jackspeak": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.3.tgz", + "integrity": "sha512-R2bUw+kVZFS/h1AZqBKrSgDmdmjApzgY0AlCPumopFiAlbUxE2gf+SCuBzQ0cP5hHmUmFYF5yw55T97Th5Kstg==", "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" + "@isaacs/cliui": "^8.0.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "engines": { + "node": ">=14" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" + "funding": { + "url": "https://github.com/sponsors/isaacs" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "argparse": "^2.0.1" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "p-locate": "^5.0.0" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "engines": { + "node": ">=10" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" }, "engines": { - "node": ">=6.9.0" + "node": ">=10" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "node_modules/log-symbols/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" + "color-convert": "^2.0.1" }, "engines": { - "node": ">=6.9.0" + "node": ">=8" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@babel/plugin-syntax-unicode-sets-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", - "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=6.9.0" + "node": ">=10" }, - "peerDependencies": { - "@babel/core": "^7.0.0" + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz", - "integrity": "sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==", + "node_modules/log-symbols/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=8" } }, - "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.15.tgz", - "integrity": "sha512-jBm1Es25Y+tVoTi5rfd5t1KLmL8ogLKpXszboWOTTtGFGz2RKnQe2yn7HbZ+kb/B8N0FVSGQo874NSlOU1T4+w==", + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dependencies": { - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.9", - "@babel/plugin-syntax-async-generators": "^7.8.4" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": "*" } }, - "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz", - "integrity": "sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==", - "dependencies": { - "@babel/helper-module-imports": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz", - "integrity": "sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, + "node_modules/minipass": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.3.tgz", + "integrity": "sha512-LhbbwCfz3vsb12j/WkWQPZfKTsgqIe1Nf/ti1pKjYESGLHIVjWU96G9/ljLH4F9mWNVhlQOm0VySdAWzf05dpg==", "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=16 || 14 >=14.17" } }, - "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.22.15.tgz", - "integrity": "sha512-G1czpdJBZCtngoK1sJgloLiOHUnkb/bLZwqVZD8kXmq0ZnVfTTWUcs9OWtp0mBtYJ+4LQY1fllqBkOIPhXmFmw==", + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" + "minimist": "^1.2.6" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "bin": { + "mkdirp": "bin/cmd.js" } }, - "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz", - "integrity": "sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ==", + "node_modules/mocha": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", + "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.4", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "5.0.1", + "ms": "2.1.3", + "nanoid": "3.3.3", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "workerpool": "6.2.1", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" }, "engines": { - "node": ">=6.9.0" + "node": ">= 14.0.0" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mochajs" } }, - "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.11.tgz", - "integrity": "sha512-GMM8gGmqI7guS/llMFk1bJDkKfn3v3C4KHK9Yg1ey5qcHcOlKb0QvcMrgzvxo+T03/4szNh5lghY+fEC98Kq9g==", + "node_modules/mocha/node_modules/anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.11", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-class-static-block": "^7.14.5" + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0" + "node": ">= 8" } }, - "node_modules/@babel/plugin-transform-classes": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.15.tgz", - "integrity": "sha512-VbbC3PGjBdE0wAWDdHM9G8Gm977pnYI0XpqMd6LrKISj8/DJXEsWqgRuTYaNE9Bv0JGhTZUzHDlMk18IpOuoqw==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.9", - "@babel/helper-split-export-declaration": "^7.22.6", - "globals": "^11.1.0" - }, + "node_modules/mocha/node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=8" } }, - "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz", - "integrity": "sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==", + "node_modules/mocha/node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/template": "^7.22.5" + "fill-range": "^7.0.1" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=8" } }, - "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.22.15.tgz", - "integrity": "sha512-HzG8sFl1ZVGTme74Nw+X01XsUTqERVQ6/RLHo3XjGRzm7XD6QTtfS3NJotVgCGy8BzkDqRjRBD8dAyJn5TuvSQ==", + "node_modules/mocha/node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" }, "engines": { - "node": ">=6.9.0" + "node": ">= 8.10.0" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "optionalDependencies": { + "fsevents": "~2.3.2" } }, - "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz", - "integrity": "sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, + "node_modules/mocha/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "engines": { - "node": ">=6.9.0" + "node": ">=10" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz", - "integrity": "sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==", + "node_modules/mocha/node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "to-regex-range": "^5.0.1" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=8" } }, - "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.11.tgz", - "integrity": "sha512-g/21plo58sfteWjaO0ZNVb+uEOkJNjAaHhbejrnBmu011l/eNDScmkbjCC3l4FKb10ViaGU4aOkFznSu2zRHgA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" - }, + "node_modules/mocha/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz", - "integrity": "sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==", + "node_modules/mocha/node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, "engines": { - "node": ">=6.9.0" + "node": "*" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.11.tgz", - "integrity": "sha512-xa7aad7q7OiT8oNZ1mU7NrISjlSkVdMbNxn9IuLZyL9AJEhs1Apba3I+u5riX1dIkdptP5EKDG5XDPByWxtehw==", + "node_modules/mocha/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + "is-glob": "^4.0.1" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">= 6" } }, - "node_modules/@babel/plugin-transform-for-of": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.15.tgz", - "integrity": "sha512-me6VGeHsx30+xh9fbDLLPi0J1HzmeIIyenoOQHuw2D4m2SAU3NrspX5XxJLBpqn5yrLzrlw2Iy3RA//Bx27iOA==", + "node_modules/mocha/node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": "*" } }, - "node_modules/@babel/plugin-transform-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz", - "integrity": "sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==", + "node_modules/mocha/node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dependencies": { - "@babel/helper-compilation-targets": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" + "binary-extensions": "^2.0.0" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=8" } }, - "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.11.tgz", - "integrity": "sha512-CxT5tCqpA9/jXFlme9xIBCc5RPtdDq3JpkkhgHQqtDdiTnTI0jtZ0QzXhr5DILeYifDPp2wvY2ad+7+hLMW5Pw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-json-strings": "^7.8.3" - }, + "node_modules/mocha/node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=0.10.0" } }, - "node_modules/@babel/plugin-transform-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz", - "integrity": "sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==", + "node_modules/mocha/node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "is-extglob": "^2.1.1" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=0.10.0" } }, - "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.11.tgz", - "integrity": "sha512-qQwRTP4+6xFCDV5k7gZBF3C31K34ut0tbEcTKxlX/0KXxm9GLcO14p570aWxFvVzx6QAfPgq7gaeIHXJC8LswQ==", + "node_modules/mocha/node_modules/minimatch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=10" } }, - "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz", - "integrity": "sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==", + "node_modules/mocha/node_modules/minimatch/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "balanced-match": "^1.0.0" } }, - "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.22.5.tgz", - "integrity": "sha512-R+PTfLTcYEmb1+kK7FNkhQ1gP4KgjpSO6HfH9+f8/yfp2Nt3ggBjiVpRwmwTlfqZLafYKJACy36yDXlEmI9HjQ==", - "dependencies": { - "@babel/helper-module-transforms": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } + "node_modules/mocha/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, - "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.15.tgz", - "integrity": "sha512-jWL4eh90w0HQOTKP2MoXXUpVxilxsB2Vl4ji69rSjS3EcZ/v4sBmn+A3NpepuJzBhOaEBbR7udonlHHn5DWidg==", - "dependencies": { - "@babel/helper-module-transforms": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-simple-access": "^7.22.5" - }, + "node_modules/mocha/node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=0.10.0" } }, - "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.11.tgz", - "integrity": "sha512-rIqHmHoMEOhI3VkVf5jQ15l539KrwhzqcBO6wdCNWPWc/JWt9ILNYNUssbRpeq0qWns8svuw8LnMNCvWBIJ8wA==", + "node_modules/mocha/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dependencies": { - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-module-transforms": "^7.22.9", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5" + "picomatch": "^2.2.1" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=8.10.0" } }, - "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz", - "integrity": "sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==", + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dependencies": { - "@babel/helper-module-transforms": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=6.9.0" + "node": ">=10" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", - "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/nan": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.28.0.tgz", + "integrity": "sha512-fTsDz99OTq2sVePhGdp4qQhggZFtKr64ZNVyVajRKtMOkJxYekplBh577PiJB12v/D3s2E5cGtOI45LWp6rnLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", + "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", + "bin": { + "nanoid": "bin/nanoid.cjs" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/@babel/plugin-transform-new-target": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz", - "integrity": "sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==", + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "wrappy": "1" } }, - "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.11.tgz", - "integrity": "sha512-YZWOw4HxXrotb5xsjMJUDlLgcDXSfO9eCmdl1bgW4+/lAGdkjaEvOnQ4p5WKKdUgSzO39dgPl0pTnfxm0OAXcg==", + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + "yocto-queue": "^0.1.0" }, "engines": { - "node": ">=6.9.0" + "node": ">=10" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.11.tgz", - "integrity": "sha512-3dzU4QGPsILdJbASKhF/V2TVP+gJya1PsueQCxIPCEcerqF21oEcrob4mzjsp2Py/1nLfF5m+xYNMDpmA8vffg==", + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" + "p-limit": "^3.0.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=10" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.15.tgz", - "integrity": "sha512-fEB+I1+gAmfAyxZcX1+ZUwLeAuuf8VIg67CTznZE0MqVFumWkh8xWtn58I4dxdVf080wn7gzWoF8vndOViJe9Q==", - "dependencies": { - "@babel/compat-data": "^7.22.9", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.22.15" - }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=8" } }, - "node_modules/@babel/plugin-transform-object-super": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz", - "integrity": "sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.5" - }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=0.10.0" } }, - "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.11.tgz", - "integrity": "sha512-rli0WxesXUeCJnMYhzAglEjLWVDF6ahb45HuprcmQuLidBJFWjNnOzssk2kuc6e33FlLaiZhG/kUIzUMWdBKaQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" - }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=8" } }, - "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.15.tgz", - "integrity": "sha512-ngQ2tBhq5vvSJw2Q2Z9i7ealNkpDMU0rGWnHPKqRZO0tzZ5tlaoz4hDvhXioOoaE0X2vfNss1djwg0DXlfu30A==", + "node_modules/path-scurry": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", + "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { - "node": ">=6.9.0" + "node": ">=16 || 14 >=14.17" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@babel/plugin-transform-parameters": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.15.tgz", - "integrity": "sha512-hjk7qKIqhyzhhUvRT683TYQOFa/4cQKwQy7ALvTpODswN40MljzNDa0YldevS6tGbxwaEKVn502JmY0dP7qEtQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz", + "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==", "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": "14 || >=16.14" } }, - "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz", - "integrity": "sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA==", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "engines": { - "node": ">=6.9.0" + "node": ">=8.6" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.11.tgz", - "integrity": "sha512-sSCbqZDBKHetvjSwpyWzhuHkmW5RummxJBVbYLkGkaiTOWGxml7SXt0iWa03bzxFIx7wOj3g/ILRd0RcJKBeSQ==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.22.11", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + "node_modules/prettier": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz", + "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==", + "bin": { + "prettier": "bin/prettier.cjs" }, "engines": { - "node": ">=6.9.0" + "node": ">=14" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz", - "integrity": "sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==", + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "safe-buffer": "^5.1.0" } }, - "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.10.tgz", - "integrity": "sha512-F28b1mDt8KcT5bUyJc/U9nwzw6cV+UmTeRlXYIl2TNqMMJif0Jeey9/RQ3C4NOd2zp0/TRsDns9ttj2L523rsw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "regenerator-transform": "^0.15.2" - }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=0.10.0" } }, - "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz", - "integrity": "sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==", + "node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" + "glob": "^7.1.3" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "bin": { + "rimraf": "bin.js" } }, - "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz", - "integrity": "sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==", + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, "engines": { - "node": ">=6.9.0" + "node": "*" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@babel/plugin-transform-spread": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz", - "integrity": "sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==", + "node_modules/safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + }, + "node_modules/serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "randombytes": "^2.1.0" } }, - "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz", - "integrity": "sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==", + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "shebang-regex": "^3.0.0" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=8" } }, - "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz", - "integrity": "sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=8" } }, - "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz", - "integrity": "sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "engines": { - "node": ">=6.9.0" + "node": ">=14" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.10.tgz", - "integrity": "sha512-lRfaRKGZCBqDlRU3UIFovdp9c9mEvlylmpod0/OatICsSfuQ9YFthRo1tpTkGsklEefZdqlEFdY4A2dwTb6ohg==", + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=8" } }, - "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz", - "integrity": "sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A==", + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=8" } }, - "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz", - "integrity": "sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=8" } }, - "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz", - "integrity": "sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg==", + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" + "ansi-regex": "^5.0.1" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" + "node": ">=8" } }, - "node_modules/@babel/preset-env": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.22.20.tgz", - "integrity": "sha512-11MY04gGC4kSzlPHRfvVkNAZhUxOvm7DCJ37hPDnUENwe06npjIRAfInEMTGSb4LZK5ZgDFkv5hw0lGebHeTyg==", - "dependencies": { - "@babel/compat-data": "^7.22.20", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.15", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.15", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.15", - "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.22.5", - "@babel/plugin-syntax-import-attributes": "^7.22.5", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5", - "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.22.5", - "@babel/plugin-transform-async-generator-functions": "^7.22.15", - "@babel/plugin-transform-async-to-generator": "^7.22.5", - "@babel/plugin-transform-block-scoped-functions": "^7.22.5", - "@babel/plugin-transform-block-scoping": "^7.22.15", - "@babel/plugin-transform-class-properties": "^7.22.5", - "@babel/plugin-transform-class-static-block": "^7.22.11", - "@babel/plugin-transform-classes": "^7.22.15", - "@babel/plugin-transform-computed-properties": "^7.22.5", - "@babel/plugin-transform-destructuring": "^7.22.15", - "@babel/plugin-transform-dotall-regex": "^7.22.5", - "@babel/plugin-transform-duplicate-keys": "^7.22.5", - "@babel/plugin-transform-dynamic-import": "^7.22.11", - "@babel/plugin-transform-exponentiation-operator": "^7.22.5", - "@babel/plugin-transform-export-namespace-from": "^7.22.11", - "@babel/plugin-transform-for-of": "^7.22.15", - "@babel/plugin-transform-function-name": "^7.22.5", - "@babel/plugin-transform-json-strings": "^7.22.11", - "@babel/plugin-transform-literals": "^7.22.5", - "@babel/plugin-transform-logical-assignment-operators": "^7.22.11", - "@babel/plugin-transform-member-expression-literals": "^7.22.5", - "@babel/plugin-transform-modules-amd": "^7.22.5", - "@babel/plugin-transform-modules-commonjs": "^7.22.15", - "@babel/plugin-transform-modules-systemjs": "^7.22.11", - "@babel/plugin-transform-modules-umd": "^7.22.5", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", - "@babel/plugin-transform-new-target": "^7.22.5", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.11", - "@babel/plugin-transform-numeric-separator": "^7.22.11", - "@babel/plugin-transform-object-rest-spread": "^7.22.15", - "@babel/plugin-transform-object-super": "^7.22.5", - "@babel/plugin-transform-optional-catch-binding": "^7.22.11", - "@babel/plugin-transform-optional-chaining": "^7.22.15", - "@babel/plugin-transform-parameters": "^7.22.15", - "@babel/plugin-transform-private-methods": "^7.22.5", - "@babel/plugin-transform-private-property-in-object": "^7.22.11", - "@babel/plugin-transform-property-literals": "^7.22.5", - "@babel/plugin-transform-regenerator": "^7.22.10", - "@babel/plugin-transform-reserved-words": "^7.22.5", - "@babel/plugin-transform-shorthand-properties": "^7.22.5", - "@babel/plugin-transform-spread": "^7.22.5", - "@babel/plugin-transform-sticky-regex": "^7.22.5", - "@babel/plugin-transform-template-literals": "^7.22.5", - "@babel/plugin-transform-typeof-symbol": "^7.22.5", - "@babel/plugin-transform-unicode-escapes": "^7.22.10", - "@babel/plugin-transform-unicode-property-regex": "^7.22.5", - "@babel/plugin-transform-unicode-regex": "^7.22.5", - "@babel/plugin-transform-unicode-sets-regex": "^7.22.5", - "@babel/preset-modules": "0.1.6-no-external-plugins", - "@babel/types": "^7.22.19", - "babel-plugin-polyfill-corejs2": "^0.4.5", - "babel-plugin-polyfill-corejs3": "^0.8.3", - "babel-plugin-polyfill-regenerator": "^0.5.2", - "core-js-compat": "^3.31.0", - "semver": "^6.3.1" - }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-env/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" + "node": ">=8" } }, - "node_modules/@babel/preset-modules": { - "version": "0.1.6-no-external-plugins", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", - "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "node_modules/string-width/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" + "ansi-regex": "^5.0.1" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + "engines": { + "node": ">=8" } }, - "node_modules/@babel/register": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.22.15.tgz", - "integrity": "sha512-V3Q3EqoQdn65RCgTLwauZaTfd1ShhwPmbBv+1dkZV/HpCGMKVyn6oFcRlI7RaKqiDQjX2Qd3AuoEguBgdjIKlg==", + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dependencies": { - "clone-deep": "^4.0.1", - "find-cache-dir": "^2.0.0", - "make-dir": "^2.1.0", - "pirates": "^4.0.5", - "source-map-support": "^0.5.16" + "ansi-regex": "^6.0.1" }, "engines": { - "node": ">=6.9.0" + "node": ">=12" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/@babel/regjsgen": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==" - }, - "node_modules/@babel/runtime": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.15.tgz", - "integrity": "sha512-T0O+aa+4w0u06iNmapipJXMV4HoUir03hpx3/YqXXhu9xim3w+dVphjFWl1OH8NbZHw5Lbm9k45drDkgq2VNNA==", + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dependencies": { - "regenerator-runtime": "^0.14.0" + "ansi-regex": "^5.0.1" }, "engines": { - "node": ">=6.9.0" + "node": ">=8" } }, - "node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", - "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" - }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "engines": { - "node": ">=6.9.0" + "node": ">=8" } }, - "node_modules/@babel/traverse": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.9.tgz", - "integrity": "sha512-PQO8sDIJ8SIwipTPiR71kJQCKQYB5NGImbOviK8K+kg5xkNSYXLBupuX9QhatFowrsvo9Hj8WgArg3W7ijNAQw==", - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.9", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.17.9", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.17.9", - "@babel/types": "^7.17.0", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "engines": { - "node": ">=6.9.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@babel/types": { - "version": "7.22.19", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.19.tgz", - "integrity": "sha512-P7LAw/LbojPzkgp5oznjE6tQEIWbp4PkkfrZDINTro9zgBRtI324/EYsiSI7lhPbpIQ+DCeR2NNmMWANGGfZsg==", + "node_modules/temp": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", + "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.19", - "to-fast-properties": "^2.0.0" + "mkdirp": "^0.5.1", + "rimraf": "~2.6.2" }, "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types/node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "engines": { - "node": ">=4" + "node": ">=6.0.0" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + "is-number": "^7.0.0" }, "engines": { - "node": ">=12" + "node": ">=8.0" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "node_modules/to-regex-range/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=0.12.0" } }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" + "node_modules/typescript": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc" }, "engines": { - "node": ">=12" + "node": ">=16.20.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" + } + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT" }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" + "isexe": "^2.0.0" }, - "engines": { - "node": ">=12" + "bin": { + "node-which": "bin/node-which" }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", "engines": { - "node": ">=6.0.0" + "node": ">= 8" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + "node_modules/workerpool": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", + "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==" }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.19", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", - "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@nicolo-ribaudo/chokidar-2": { - "version": "2.1.8-no-fsevents.3", - "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz", - "integrity": "sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==", - "optional": true - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, "engines": { - "node": ">=12" + "node": ">=10" }, "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dependencies": { - "color-convert": "^1.9.0" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": ">=4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/ansi-styles/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" } }, - "node_modules/ansi-styles/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", - "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dependencies": { - "call-bind": "^1.0.2", - "is-array-buffer": "^3.0.1" + "color-convert": "^2.0.1" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.reduce": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/array.prototype.reduce/-/array.prototype.reduce-1.0.6.tgz", - "integrity": "sha512-UW+Mz8LG/sPSU8jRDCjVr6J/ZKAGpHfwrZ6kWTG5qCxIEiXdVshqGnu5vEZA8S1y6X4aCSbQZ0/EEsfvEvBiSg==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-array-method-boxes-properly": "^1.0.0", - "is-string": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", - "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "is-array-buffer": "^3.0.2", - "is-shared-array-buffer": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.5.tgz", - "integrity": "sha512-19hwUH5FKl49JEsvyTcoHakh6BE0wgXLLptIyKZ3PijHc/Ci521wygORCUCCred+E/twuqRyAkE02BAWPmsHOg==", - "dependencies": { - "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.4.2", - "semver": "^6.3.1" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.3.tgz", - "integrity": "sha512-z41XaniZL26WLrvjy7soabMXrfPWARN25PZoriDEiLMxAp50AUW3t35BGQUMg5xK3UrpVTtagIDklxYa+MhiNA==", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.2", - "core-js-compat": "^3.31.0" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.2.tgz", - "integrity": "sha512-tAlOptU0Xj34V1Y2PNTL4Y0FOJMDB6bZmoW39FeCQIhigGLkqu3Fj6uiXpxIf6Ij274ENdYx64y6Au+ZKlb1IA==", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.2" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==" - }, - "node_modules/browserslist": { - "version": "4.21.10", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz", - "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "caniuse-lite": "^1.0.30001517", - "electron-to-chromium": "^1.4.477", - "node-releases": "^2.0.13", - "update-browserslist-db": "^1.0.11" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" - }, - "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001534", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001534.tgz", - "integrity": "sha512-vlPVrhsCS7XaSh2VvWluIQEzVhefrUQcEsQWSS5A5V+dM07uv1qHeQzAOTGIMy9i3e9bH15+muvI/UHojVgS/Q==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ] - }, - "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "dependencies": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/colors": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", - "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "engines": { - "node": ">= 6" - } - }, - "node_modules/commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "node_modules/convert-source-map": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", - "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", - "dependencies": { - "safe-buffer": "~5.1.1" - } - }, - "node_modules/core-js": { - "version": "3.32.2", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.32.2.tgz", - "integrity": "sha512-pxXSw1mYZPDGvTQqEc5vgIb83jGQKFGYWY76z4a7weZXUolw3G+OvpZqSRcfYOoOVUQJYEPsWeQK8pKEnUtWxQ==", - "hasInstallScript": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-js-compat": { - "version": "3.32.2", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.32.2.tgz", - "integrity": "sha512-+GjlguTDINOijtVRUxrQOv3kfu9rl+qPNdX2LTbJ/ZyVTuxK+ksVSAGX1nHstu4hrv1En/uPTtWgq2gI5wt4AQ==", - "dependencies": { - "browserslist": "^4.21.10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decamelize": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", - "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/define-data-property": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.0.tgz", - "integrity": "sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g==", - "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" - }, - "node_modules/electron-to-chromium": { - "version": "1.4.523", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.523.tgz", - "integrity": "sha512-9AreocSUWnzNtvLcbpng6N+GkXnCcBR80IQkxRC9Dfdyg4gaWNUPBujAHUpKkiUkoSoR9UlhA4zD/IgBklmhzg==" - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "node_modules/es-abstract": { - "version": "1.22.2", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.2.tgz", - "integrity": "sha512-YoxfFcDmhjOgWPWsV13+2RNjq1F6UQnfs+8TftwNqtzlmFzEXvlUwdrNrYeaizfjQzRMxkZ6ElWMOJIFKdVqwA==", - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "arraybuffer.prototype.slice": "^1.0.2", - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "es-set-tostringtag": "^2.0.1", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.1", - "get-symbol-description": "^1.0.0", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", - "has": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.5", - "is-array-buffer": "^3.0.2", - "is-callable": "^1.2.7", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.12", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.3", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "safe-array-concat": "^1.0.1", - "safe-regex-test": "^1.0.0", - "string.prototype.trim": "^1.2.8", - "string.prototype.trimend": "^1.0.7", - "string.prototype.trimstart": "^1.0.7", - "typed-array-buffer": "^1.0.0", - "typed-array-byte-length": "^1.0.0", - "typed-array-byte-offset": "^1.0.0", - "typed-array-length": "^1.0.4", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.11" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-array-method-boxes-properly": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", - "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==" - }, - "node_modules/es-set-tostringtag": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", - "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==", - "dependencies": { - "get-intrinsic": "^1.1.3", - "has": "^1.0.3", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/find-cache-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", - "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", - "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^2.0.0", - "pkg-dir": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "bin": { - "flat": "cli.js" - } - }, - "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "dependencies": { - "is-callable": "^1.1.3" - } - }, - "node_modules/foreground-child": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", - "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/fs-readdir-recursive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", - "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==" - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "node_modules/function.prototype.name": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", - "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "functions-have-names": "^1.2.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "peer": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/glob": { - "version": "10.3.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.4.tgz", - "integrity": "sha512-6LFElP3A+i/Q8XQKEvZjkEWEOTgAIALR9AO2rwT8bgPhDd1anmqDJDZ6lLddI4ehxxxR1S5RIqKe1uapMQfYaQ==", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.0.3", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - }, - "bin": { - "glob": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "engines": { - "node": ">=4" - } - }, - "node_modules/globalthis": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", - "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", - "dependencies": { - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", - "dependencies": { - "get-intrinsic": "^1.1.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "bin": { - "he": "bin/he" - } - }, - "node_modules/homedir-polyfill": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", - "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", - "dependencies": { - "parse-passwd": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "node_modules/internal-slot": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", - "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", - "dependencies": { - "get-intrinsic": "^1.2.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", - "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", - "is-typed-array": "^1.1.10" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "dependencies": { - "has-bigints": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", - "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", - "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", - "dependencies": { - "which-typed-array": "^1.1.11" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" - }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/jackspeak": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.3.tgz", - "integrity": "sha512-R2bUw+kVZFS/h1AZqBKrSgDmdmjApzgY0AlCPumopFiAlbUxE2gf+SCuBzQ0cP5hHmUmFYF5yw55T97Th5Kstg==", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", - "bin": { - "jsesc": "bin/jsesc" - } - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "peer": true, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" - }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-symbols/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/log-symbols/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/log-symbols/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dependencies": { - "pify": "^4.0.1", - "semver": "^5.6.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.3.tgz", - "integrity": "sha512-LhbbwCfz3vsb12j/WkWQPZfKTsgqIe1Nf/ti1pKjYESGLHIVjWU96G9/ljLH4F9mWNVhlQOm0VySdAWzf05dpg==", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/mocha": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", - "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", - "dependencies": { - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.3", - "debug": "4.3.4", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.2.0", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "5.0.1", - "ms": "2.1.3", - "nanoid": "3.3.3", - "serialize-javascript": "6.0.0", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "workerpool": "6.2.1", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" - }, - "bin": { - "_mocha": "bin/_mocha", - "mocha": "bin/mocha.js" - }, - "engines": { - "node": ">= 14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mochajs" - } - }, - "node_modules/mocha/node_modules/anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/mocha/node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "engines": { - "node": ">=8" - } - }, - "node_modules/mocha/node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/mocha/node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/mocha/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mocha/node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/mocha/node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/mocha/node_modules/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/mocha/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/mocha/node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/mocha/node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/mocha/node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/mocha/node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/mocha/node_modules/minimatch": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", - "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/mocha/node_modules/minimatch/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/mocha/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/mocha/node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/mocha/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/mocha/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/nan": { - "version": "2.18.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz", - "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==" - }, - "node_modules/nanoid": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", - "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/node-environment-flags": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.6.tgz", - "integrity": "sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw==", - "dependencies": { - "object.getownpropertydescriptors": "^2.0.3", - "semver": "^5.7.0" - } - }, - "node_modules/node-releases": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", - "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==" - }, - "node_modules/object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", - "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.getownpropertydescriptors": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.7.tgz", - "integrity": "sha512-PrJz0C2xJ58FNn11XV2lr4Jt5Gzl94qpy9Lu0JlfEj14z88sqbSBJCBEzdlNUCzY2gburhbrwOZ5BHCmuNUy0g==", - "dependencies": { - "array.prototype.reduce": "^1.0.6", - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "safe-array-concat": "^1.0.0" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-passwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", - "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" - }, - "node_modules/path-scurry": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", - "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", - "dependencies": { - "lru-cache": "^9.1.1 || ^10.0.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz", - "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==", - "engines": { - "node": "14 || >=16.14" - } - }, - "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "engines": { - "node": ">=6" - } - }, - "node_modules/pirates": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", - "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkg-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", - "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", - "dependencies": { - "find-up": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-dir/node_modules/find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dependencies": { - "locate-path": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-dir/node_modules/locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-dir/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-dir/node_modules/p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dependencies": { - "p-limit": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-dir/node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "engines": { - "node": ">=4" - } - }, - "node_modules/prettier": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz", - "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/regenerate": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==" - }, - "node_modules/regenerate-unicode-properties": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", - "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", - "dependencies": { - "regenerate": "^1.4.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regenerator-runtime": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", - "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" - }, - "node_modules/regenerator-transform": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", - "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", - "dependencies": { - "@babel/runtime": "^7.8.4" - } - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", - "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "set-function-name": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regexpu-core": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", - "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", - "dependencies": { - "@babel/regjsgen": "^0.8.0", - "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.1.0", - "regjsparser": "^0.9.1", - "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regjsparser": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", - "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", - "dependencies": { - "jsesc": "~0.5.0" - }, - "bin": { - "regjsparser": "bin/parser" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.6", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz", - "integrity": "sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/rimraf/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/safe-array-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", - "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", - "has-symbols": "^1.0.3", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-buffer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" - }, - "node_modules/safe-regex-test": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", - "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "is-regex": "^1.1.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/set-function-name": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", - "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", - "dependencies": { - "define-data-property": "^1.0.1", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dependencies": { - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/source-map-support/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string.prototype.trim": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", - "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", - "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", - "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/supports-color/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "engines": { - "node": ">=4" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/temp": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", - "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", - "dependencies": { - "mkdirp": "^0.5.1", - "rimraf": "~2.6.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/to-regex-range/node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/typed-array-buffer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", - "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", - "is-typed-array": "^1.1.10" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", - "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", - "dependencies": { - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", - "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", - "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", - "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", - "dependencies": { - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "is-typed-array": "^1.1.9" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", - "dependencies": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/unicode-canonical-property-names-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", - "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", - "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", - "dependencies": { - "unicode-canonical-property-names-ecmascript": "^2.0.0", - "unicode-property-aliases-ecmascript": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", - "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-property-aliases-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", - "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", - "engines": { - "node": ">=4" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", - "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/v8flags": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", - "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==", - "dependencies": { - "homedir-polyfill": "^1.0.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", - "integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==", - "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/workerpool": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", - "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==" - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" - }, - "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-unparser": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", - "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", - "dependencies": { - "camelcase": "^6.0.0", - "decamelize": "^4.0.0", - "flat": "^5.0.2", - "is-plain-obj": "^2.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - }, - "dependencies": { - "@ampproject/remapping": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.1.2.tgz", - "integrity": "sha512-hoyByceqwKirw7w3Z7gnIIZC3Wx3J484Y3L/cMpXFbr7d9ZQj2mODrirNzcJa+SM3UlpWXYvKV4RlRpFXlWgXg==", - "peer": true, - "requires": { - "@jridgewell/trace-mapping": "^0.3.0" - } - }, - "@babel/cli": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.22.15.tgz", - "integrity": "sha512-prtg5f6zCERIaECeTZzd2fMtVjlfjhUcO+fBLQ6DXXdq5FljN+excVitJ2nogsusdf31LeqkjAfXZ7Xq+HmN8g==", - "requires": { - "@jridgewell/trace-mapping": "^0.3.17", - "@nicolo-ribaudo/chokidar-2": "2.1.8-no-fsevents.3", - "chokidar": "^3.4.0", - "commander": "^4.0.1", - "convert-source-map": "^1.1.0", - "fs-readdir-recursive": "^1.1.0", - "glob": "^7.2.0", - "make-dir": "^2.1.0", - "slash": "^2.0.0" - }, - "dependencies": { - "anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - "optional": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "optional": true - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "optional": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "optional": true, - "requires": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "fsevents": "~2.3.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "optional": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "optional": true - }, - "glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "optional": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "optional": true, - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "optional": true - }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "optional": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "optional": true - }, - "readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "optional": true, - "requires": { - "picomatch": "^2.2.1" - } - }, - "slash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==" - } - } - }, - "@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", - "requires": { - "@babel/highlight": "^7.22.13", - "chalk": "^2.4.2" - } - }, - "@babel/compat-data": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.20.tgz", - "integrity": "sha512-BQYjKbpXjoXwFW5jGqiizJQQT/aC7pFm9Ok1OWssonuguICi264lbgMzRp2ZMmRSlfkX6DsWDDcsrctK8Rwfiw==" - }, - "@babel/core": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.17.9.tgz", - "integrity": "sha512-5ug+SfZCpDAkVp9SFIZAzlW18rlzsOcJGaetCjkySnrXXDUw9AR8cDUm1iByTmdWM6yxX6/zycaV76w3YTF2gw==", - "peer": true, - "requires": { - "@ampproject/remapping": "^2.1.0", - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.9", - "@babel/helper-compilation-targets": "^7.17.7", - "@babel/helper-module-transforms": "^7.17.7", - "@babel/helpers": "^7.17.9", - "@babel/parser": "^7.17.9", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.9", - "@babel/types": "^7.17.0", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.1", - "semver": "^6.3.0" - }, - "dependencies": { - "semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "peer": true - } - } - }, - "@babel/generator": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.9.tgz", - "integrity": "sha512-rAdDousTwxbIxbz5I7GEQ3lUip+xVCXooZNbsydCWs3xA7ZsYOv+CFRdzGxRX78BmQHu9B1Eso59AOZQOJDEdQ==", - "peer": true, - "requires": { - "@babel/types": "^7.17.0", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - }, - "dependencies": { - "jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "peer": true - } - } - }, - "@babel/helper-annotate-as-pure": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", - "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", - "requires": { - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz", - "integrity": "sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==", - "requires": { - "@babel/types": "^7.22.15" - } - }, - "@babel/helper-compilation-targets": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", - "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", - "requires": { - "@babel/compat-data": "^7.22.9", - "@babel/helper-validator-option": "^7.22.15", - "browserslist": "^4.21.9", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "dependencies": { - "semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" - } - } - }, - "@babel/helper-create-class-features-plugin": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz", - "integrity": "sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==", - "requires": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-member-expression-to-functions": "^7.22.15", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "semver": "^6.3.1" - }, - "dependencies": { - "semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" - } - } - }, - "@babel/helper-create-regexp-features-plugin": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz", - "integrity": "sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==", - "requires": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "regexpu-core": "^5.3.1", - "semver": "^6.3.1" - }, - "dependencies": { - "semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" - } - } - }, - "@babel/helper-define-polyfill-provider": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.2.tgz", - "integrity": "sha512-k0qnnOqHn5dK9pZpfD5XXZ9SojAITdCKRn2Lp6rnDGzIbaP0rHyMPk/4wsSxVBVz4RfN0q6VpXWP2pDGIoQ7hw==", - "requires": { - "@babel/helper-compilation-targets": "^7.22.6", - "@babel/helper-plugin-utils": "^7.22.5", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2" - } - }, - "@babel/helper-environment-visitor": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==" - }, - "@babel/helper-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", - "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", - "requires": { - "@babel/template": "^7.22.5", - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", - "requires": { - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-member-expression-to-functions": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.15.tgz", - "integrity": "sha512-qLNsZbgrNh0fDQBCPocSL8guki1hcPvltGDv/NxvUoABwFq7GkKSu1nRXeJkVZc+wJvne2E0RKQz+2SQrz6eAA==", - "requires": { - "@babel/types": "^7.22.15" - } - }, - "@babel/helper-module-imports": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", - "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", - "requires": { - "@babel/types": "^7.22.15" - } - }, - "@babel/helper-module-transforms": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.20.tgz", - "integrity": "sha512-dLT7JVWIUUxKOs1UnJUBR3S70YK+pKX6AbJgB2vMIvEkZkrfJDbYDJesnPshtKV4LhDOR3Oc5YULeDizRek+5A==", - "requires": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^7.22.20" - } - }, - "@babel/helper-optimise-call-expression": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", - "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", - "requires": { - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", - "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==" - }, - "@babel/helper-remap-async-to-generator": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz", - "integrity": "sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==", - "requires": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-wrap-function": "^7.22.20" - } - }, - "@babel/helper-replace-supers": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz", - "integrity": "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==", - "requires": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-member-expression-to-functions": "^7.22.15", - "@babel/helper-optimise-call-expression": "^7.22.5" - } - }, - "@babel/helper-simple-access": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", - "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", - "requires": { - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", - "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", - "requires": { - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", - "requires": { - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==" - }, - "@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==" - }, - "@babel/helper-validator-option": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", - "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==" - }, - "@babel/helper-wrap-function": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.20.tgz", - "integrity": "sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==", - "requires": { - "@babel/helper-function-name": "^7.22.5", - "@babel/template": "^7.22.15", - "@babel/types": "^7.22.19" - } - }, - "@babel/helpers": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.17.9.tgz", - "integrity": "sha512-cPCt915ShDWUEzEp3+UNRktO2n6v49l5RSnG9M5pS24hA+2FAc5si+Pn1i4VVbQQ+jh+bIZhPFQOJOzbrOYY1Q==", - "peer": true, - "requires": { - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.9", - "@babel/types": "^7.17.0" - } - }, - "@babel/highlight": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", - "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", - "requires": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" - } - }, - "@babel/node": { - "version": "7.22.19", - "resolved": "https://registry.npmjs.org/@babel/node/-/node-7.22.19.tgz", - "integrity": "sha512-VsKSO9aEHdO16NdtqkJfrXZ9Sxlna1BVnBbToWr1KGdI3cyIk6KqOoa8mWvpK280lJDOwJqxvnl994KmLhq1Yw==", - "requires": { - "@babel/register": "^7.22.15", - "commander": "^4.0.1", - "core-js": "^3.30.2", - "node-environment-flags": "^1.0.5", - "regenerator-runtime": "^0.14.0", - "v8flags": "^3.1.1" - } - }, - "@babel/parser": { - "version": "7.22.16", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.16.tgz", - "integrity": "sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA==" - }, - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.15.tgz", - "integrity": "sha512-FB9iYlz7rURmRJyXRKEnalYPPdn87H5no108cyuQQyMwlpJ2SJtpIUBI27kdTin956pz+LPypkPVPUTlxOmrsg==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.15.tgz", - "integrity": "sha512-Hyph9LseGvAeeXzikV88bczhsrLrIZqDPxO+sSmAunMPaGrBGhfMWzCPYTtiW9t+HzSE2wtV8e5cc5P6r1xMDQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.22.15" - } - }, - "@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0-placeholder-for-preset-env.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", - "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", - "requires": {} - }, - "@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "requires": { - "@babel/helper-plugin-utils": "^7.12.13" - } - }, - "@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", - "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-export-namespace-from": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", - "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", - "requires": { - "@babel/helper-plugin-utils": "^7.8.3" - } - }, - "@babel/plugin-syntax-import-assertions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz", - "integrity": "sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-syntax-import-attributes": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz", - "integrity": "sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-syntax-unicode-sets-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", - "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-arrow-functions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz", - "integrity": "sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-async-generator-functions": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.15.tgz", - "integrity": "sha512-jBm1Es25Y+tVoTi5rfd5t1KLmL8ogLKpXszboWOTTtGFGz2RKnQe2yn7HbZ+kb/B8N0FVSGQo874NSlOU1T4+w==", - "requires": { - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.9", - "@babel/plugin-syntax-async-generators": "^7.8.4" - } - }, - "@babel/plugin-transform-async-to-generator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz", - "integrity": "sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==", - "requires": { - "@babel/helper-module-imports": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.5" - } - }, - "@babel/plugin-transform-block-scoped-functions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz", - "integrity": "sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-block-scoping": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.22.15.tgz", - "integrity": "sha512-G1czpdJBZCtngoK1sJgloLiOHUnkb/bLZwqVZD8kXmq0ZnVfTTWUcs9OWtp0mBtYJ+4LQY1fllqBkOIPhXmFmw==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-class-properties": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz", - "integrity": "sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ==", - "requires": { - "@babel/helper-create-class-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-class-static-block": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.11.tgz", - "integrity": "sha512-GMM8gGmqI7guS/llMFk1bJDkKfn3v3C4KHK9Yg1ey5qcHcOlKb0QvcMrgzvxo+T03/4szNh5lghY+fEC98Kq9g==", - "requires": { - "@babel/helper-create-class-features-plugin": "^7.22.11", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-class-static-block": "^7.14.5" - } - }, - "@babel/plugin-transform-classes": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.15.tgz", - "integrity": "sha512-VbbC3PGjBdE0wAWDdHM9G8Gm977pnYI0XpqMd6LrKISj8/DJXEsWqgRuTYaNE9Bv0JGhTZUzHDlMk18IpOuoqw==", - "requires": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.9", - "@babel/helper-split-export-declaration": "^7.22.6", - "globals": "^11.1.0" - } - }, - "@babel/plugin-transform-computed-properties": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz", - "integrity": "sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/template": "^7.22.5" - } - }, - "@babel/plugin-transform-destructuring": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.22.15.tgz", - "integrity": "sha512-HzG8sFl1ZVGTme74Nw+X01XsUTqERVQ6/RLHo3XjGRzm7XD6QTtfS3NJotVgCGy8BzkDqRjRBD8dAyJn5TuvSQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-dotall-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz", - "integrity": "sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==", - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-duplicate-keys": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz", - "integrity": "sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-dynamic-import": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.11.tgz", - "integrity": "sha512-g/21plo58sfteWjaO0ZNVb+uEOkJNjAaHhbejrnBmu011l/eNDScmkbjCC3l4FKb10ViaGU4aOkFznSu2zRHgA==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" - } - }, - "@babel/plugin-transform-exponentiation-operator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz", - "integrity": "sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==", - "requires": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-export-namespace-from": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.11.tgz", - "integrity": "sha512-xa7aad7q7OiT8oNZ1mU7NrISjlSkVdMbNxn9IuLZyL9AJEhs1Apba3I+u5riX1dIkdptP5EKDG5XDPByWxtehw==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" - } - }, - "@babel/plugin-transform-for-of": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.15.tgz", - "integrity": "sha512-me6VGeHsx30+xh9fbDLLPi0J1HzmeIIyenoOQHuw2D4m2SAU3NrspX5XxJLBpqn5yrLzrlw2Iy3RA//Bx27iOA==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz", - "integrity": "sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==", - "requires": { - "@babel/helper-compilation-targets": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-json-strings": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.11.tgz", - "integrity": "sha512-CxT5tCqpA9/jXFlme9xIBCc5RPtdDq3JpkkhgHQqtDdiTnTI0jtZ0QzXhr5DILeYifDPp2wvY2ad+7+hLMW5Pw==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-json-strings": "^7.8.3" - } - }, - "@babel/plugin-transform-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz", - "integrity": "sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-logical-assignment-operators": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.11.tgz", - "integrity": "sha512-qQwRTP4+6xFCDV5k7gZBF3C31K34ut0tbEcTKxlX/0KXxm9GLcO14p570aWxFvVzx6QAfPgq7gaeIHXJC8LswQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" - } - }, - "@babel/plugin-transform-member-expression-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz", - "integrity": "sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-modules-amd": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.22.5.tgz", - "integrity": "sha512-R+PTfLTcYEmb1+kK7FNkhQ1gP4KgjpSO6HfH9+f8/yfp2Nt3ggBjiVpRwmwTlfqZLafYKJACy36yDXlEmI9HjQ==", - "requires": { - "@babel/helper-module-transforms": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-modules-commonjs": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.15.tgz", - "integrity": "sha512-jWL4eh90w0HQOTKP2MoXXUpVxilxsB2Vl4ji69rSjS3EcZ/v4sBmn+A3NpepuJzBhOaEBbR7udonlHHn5DWidg==", - "requires": { - "@babel/helper-module-transforms": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-simple-access": "^7.22.5" - } - }, - "@babel/plugin-transform-modules-systemjs": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.11.tgz", - "integrity": "sha512-rIqHmHoMEOhI3VkVf5jQ15l539KrwhzqcBO6wdCNWPWc/JWt9ILNYNUssbRpeq0qWns8svuw8LnMNCvWBIJ8wA==", - "requires": { - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-module-transforms": "^7.22.9", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5" - } - }, - "@babel/plugin-transform-modules-umd": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz", - "integrity": "sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==", - "requires": { - "@babel/helper-module-transforms": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", - "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-new-target": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz", - "integrity": "sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.11.tgz", - "integrity": "sha512-YZWOw4HxXrotb5xsjMJUDlLgcDXSfO9eCmdl1bgW4+/lAGdkjaEvOnQ4p5WKKdUgSzO39dgPl0pTnfxm0OAXcg==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" - } - }, - "@babel/plugin-transform-numeric-separator": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.11.tgz", - "integrity": "sha512-3dzU4QGPsILdJbASKhF/V2TVP+gJya1PsueQCxIPCEcerqF21oEcrob4mzjsp2Py/1nLfF5m+xYNMDpmA8vffg==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" - } - }, - "@babel/plugin-transform-object-rest-spread": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.15.tgz", - "integrity": "sha512-fEB+I1+gAmfAyxZcX1+ZUwLeAuuf8VIg67CTznZE0MqVFumWkh8xWtn58I4dxdVf080wn7gzWoF8vndOViJe9Q==", - "requires": { - "@babel/compat-data": "^7.22.9", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.22.15" - } - }, - "@babel/plugin-transform-object-super": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz", - "integrity": "sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.5" - } - }, - "@babel/plugin-transform-optional-catch-binding": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.11.tgz", - "integrity": "sha512-rli0WxesXUeCJnMYhzAglEjLWVDF6ahb45HuprcmQuLidBJFWjNnOzssk2kuc6e33FlLaiZhG/kUIzUMWdBKaQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" - } - }, - "@babel/plugin-transform-optional-chaining": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.15.tgz", - "integrity": "sha512-ngQ2tBhq5vvSJw2Q2Z9i7ealNkpDMU0rGWnHPKqRZO0tzZ5tlaoz4hDvhXioOoaE0X2vfNss1djwg0DXlfu30A==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" - } - }, - "@babel/plugin-transform-parameters": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.15.tgz", - "integrity": "sha512-hjk7qKIqhyzhhUvRT683TYQOFa/4cQKwQy7ALvTpODswN40MljzNDa0YldevS6tGbxwaEKVn502JmY0dP7qEtQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-private-methods": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz", - "integrity": "sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA==", - "requires": { - "@babel/helper-create-class-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-private-property-in-object": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.11.tgz", - "integrity": "sha512-sSCbqZDBKHetvjSwpyWzhuHkmW5RummxJBVbYLkGkaiTOWGxml7SXt0iWa03bzxFIx7wOj3g/ILRd0RcJKBeSQ==", - "requires": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.22.11", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" - } - }, - "@babel/plugin-transform-property-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz", - "integrity": "sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-regenerator": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.10.tgz", - "integrity": "sha512-F28b1mDt8KcT5bUyJc/U9nwzw6cV+UmTeRlXYIl2TNqMMJif0Jeey9/RQ3C4NOd2zp0/TRsDns9ttj2L523rsw==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "regenerator-transform": "^0.15.2" - } - }, - "@babel/plugin-transform-reserved-words": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz", - "integrity": "sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-shorthand-properties": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz", - "integrity": "sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-spread": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz", - "integrity": "sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" - } - }, - "@babel/plugin-transform-sticky-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz", - "integrity": "sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "@babel/plugin-transform-template-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz", - "integrity": "sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, - "@babel/plugin-transform-typeof-symbol": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz", - "integrity": "sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" } }, - "@babel/plugin-transform-unicode-escapes": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.10.tgz", - "integrity": "sha512-lRfaRKGZCBqDlRU3UIFovdp9c9mEvlylmpod0/OatICsSfuQ9YFthRo1tpTkGsklEefZdqlEFdY4A2dwTb6ohg==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "@babel/plugin-transform-unicode-property-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz", - "integrity": "sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A==", - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, - "@babel/plugin-transform-unicode-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz", - "integrity": "sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==", - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - } + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, - "@babel/plugin-transform-unicode-sets-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz", - "integrity": "sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg==", - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" } }, - "@babel/preset-env": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.22.20.tgz", - "integrity": "sha512-11MY04gGC4kSzlPHRfvVkNAZhUxOvm7DCJ37hPDnUENwe06npjIRAfInEMTGSb4LZK5ZgDFkv5hw0lGebHeTyg==", - "requires": { - "@babel/compat-data": "^7.22.20", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.15", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.15", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.15", - "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.22.5", - "@babel/plugin-syntax-import-attributes": "^7.22.5", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5", - "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.22.5", - "@babel/plugin-transform-async-generator-functions": "^7.22.15", - "@babel/plugin-transform-async-to-generator": "^7.22.5", - "@babel/plugin-transform-block-scoped-functions": "^7.22.5", - "@babel/plugin-transform-block-scoping": "^7.22.15", - "@babel/plugin-transform-class-properties": "^7.22.5", - "@babel/plugin-transform-class-static-block": "^7.22.11", - "@babel/plugin-transform-classes": "^7.22.15", - "@babel/plugin-transform-computed-properties": "^7.22.5", - "@babel/plugin-transform-destructuring": "^7.22.15", - "@babel/plugin-transform-dotall-regex": "^7.22.5", - "@babel/plugin-transform-duplicate-keys": "^7.22.5", - "@babel/plugin-transform-dynamic-import": "^7.22.11", - "@babel/plugin-transform-exponentiation-operator": "^7.22.5", - "@babel/plugin-transform-export-namespace-from": "^7.22.11", - "@babel/plugin-transform-for-of": "^7.22.15", - "@babel/plugin-transform-function-name": "^7.22.5", - "@babel/plugin-transform-json-strings": "^7.22.11", - "@babel/plugin-transform-literals": "^7.22.5", - "@babel/plugin-transform-logical-assignment-operators": "^7.22.11", - "@babel/plugin-transform-member-expression-literals": "^7.22.5", - "@babel/plugin-transform-modules-amd": "^7.22.5", - "@babel/plugin-transform-modules-commonjs": "^7.22.15", - "@babel/plugin-transform-modules-systemjs": "^7.22.11", - "@babel/plugin-transform-modules-umd": "^7.22.5", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", - "@babel/plugin-transform-new-target": "^7.22.5", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.11", - "@babel/plugin-transform-numeric-separator": "^7.22.11", - "@babel/plugin-transform-object-rest-spread": "^7.22.15", - "@babel/plugin-transform-object-super": "^7.22.5", - "@babel/plugin-transform-optional-catch-binding": "^7.22.11", - "@babel/plugin-transform-optional-chaining": "^7.22.15", - "@babel/plugin-transform-parameters": "^7.22.15", - "@babel/plugin-transform-private-methods": "^7.22.5", - "@babel/plugin-transform-private-property-in-object": "^7.22.11", - "@babel/plugin-transform-property-literals": "^7.22.5", - "@babel/plugin-transform-regenerator": "^7.22.10", - "@babel/plugin-transform-reserved-words": "^7.22.5", - "@babel/plugin-transform-shorthand-properties": "^7.22.5", - "@babel/plugin-transform-spread": "^7.22.5", - "@babel/plugin-transform-sticky-regex": "^7.22.5", - "@babel/plugin-transform-template-literals": "^7.22.5", - "@babel/plugin-transform-typeof-symbol": "^7.22.5", - "@babel/plugin-transform-unicode-escapes": "^7.22.10", - "@babel/plugin-transform-unicode-property-regex": "^7.22.5", - "@babel/plugin-transform-unicode-regex": "^7.22.5", - "@babel/plugin-transform-unicode-sets-regex": "^7.22.5", - "@babel/preset-modules": "0.1.6-no-external-plugins", - "@babel/types": "^7.22.19", - "babel-plugin-polyfill-corejs2": "^0.4.5", - "babel-plugin-polyfill-corejs3": "^0.8.3", - "babel-plugin-polyfill-regenerator": "^0.5.2", - "core-js-compat": "^3.31.0", - "semver": "^6.3.1" - }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "dependencies": { - "semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" - } - } - }, - "@babel/preset-modules": { - "version": "0.1.6-no-external-plugins", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", - "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" } }, - "@babel/register": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.22.15.tgz", - "integrity": "sha512-V3Q3EqoQdn65RCgTLwauZaTfd1ShhwPmbBv+1dkZV/HpCGMKVyn6oFcRlI7RaKqiDQjX2Qd3AuoEguBgdjIKlg==", - "requires": { - "clone-deep": "^4.0.1", - "find-cache-dir": "^2.0.0", - "make-dir": "^2.1.0", - "pirates": "^4.0.5", - "source-map-support": "^0.5.16" - } - }, - "@babel/regjsgen": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==" - }, - "@babel/runtime": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.15.tgz", - "integrity": "sha512-T0O+aa+4w0u06iNmapipJXMV4HoUir03hpx3/YqXXhu9xim3w+dVphjFWl1OH8NbZHw5Lbm9k45drDkgq2VNNA==", - "requires": { - "regenerator-runtime": "^0.14.0" + "node_modules/yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "engines": { + "node": ">=10" } }, - "@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", - "requires": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" } }, - "@babel/traverse": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.9.tgz", - "integrity": "sha512-PQO8sDIJ8SIwipTPiR71kJQCKQYB5NGImbOviK8K+kg5xkNSYXLBupuX9QhatFowrsvo9Hj8WgArg3W7ijNAQw==", - "peer": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.9", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.17.9", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.17.9", - "@babel/types": "^7.17.0", - "debug": "^4.1.0", - "globals": "^11.1.0" - } - }, - "@babel/types": { - "version": "7.22.19", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.19.tgz", - "integrity": "sha512-P7LAw/LbojPzkgp5oznjE6tQEIWbp4PkkfrZDINTro9zgBRtI324/EYsiSI7lhPbpIQ+DCeR2NNmMWANGGfZsg==", - "requires": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.19", - "to-fast-properties": "^2.0.0" + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "engines": { + "node": ">=10" }, - "dependencies": { - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" - } + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } - }, + } + }, + "dependencies": { "@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -6047,35 +1806,159 @@ } } }, - "@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==" - }, - "@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" - }, - "@jridgewell/trace-mapping": { - "version": "0.3.19", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", - "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", + "@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true + }, + "@types/node": { + "version": "26.1.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", + "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", + "dev": true, "requires": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "undici-types": "~8.3.0" } }, - "@nicolo-ribaudo/chokidar-2": { - "version": "2.1.8-no-fsevents.3", - "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz", - "integrity": "sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==", + "@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "dev": true, "optional": true }, - "@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "dev": true, + "optional": true + }, + "@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "dev": true, + "optional": true + }, + "@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "dev": true, + "optional": true + }, + "@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "dev": true, + "optional": true + }, + "@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "dev": true, + "optional": true + }, + "@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "dev": true, + "optional": true + }, + "@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "dev": true, + "optional": true + }, + "@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "dev": true, + "optional": true + }, + "@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "dev": true, + "optional": true + }, + "@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "dev": true, + "optional": true + }, + "@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "dev": true, + "optional": true + }, + "@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "dev": true, + "optional": true + }, + "@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "dev": true, + "optional": true + }, + "@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "dev": true, + "optional": true + }, + "@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "dev": true, + "optional": true + }, + "@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "dev": true, + "optional": true + }, + "@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "dev": true, + "optional": true + }, + "@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "dev": true, + "optional": true + }, + "@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "dev": true, "optional": true }, "ansi-colors": { @@ -6088,108 +1971,11 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==" }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - }, - "dependencies": { - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - } - } - }, "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, - "array-buffer-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", - "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", - "requires": { - "call-bind": "^1.0.2", - "is-array-buffer": "^3.0.1" - } - }, - "array.prototype.reduce": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/array.prototype.reduce/-/array.prototype.reduce-1.0.6.tgz", - "integrity": "sha512-UW+Mz8LG/sPSU8jRDCjVr6J/ZKAGpHfwrZ6kWTG5qCxIEiXdVshqGnu5vEZA8S1y6X4aCSbQZ0/EEsfvEvBiSg==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-array-method-boxes-properly": "^1.0.0", - "is-string": "^1.0.7" - } - }, - "arraybuffer.prototype.slice": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", - "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", - "requires": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "is-array-buffer": "^3.0.2", - "is-shared-array-buffer": "^1.0.2" - } - }, - "available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==" - }, - "babel-plugin-polyfill-corejs2": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.5.tgz", - "integrity": "sha512-19hwUH5FKl49JEsvyTcoHakh6BE0wgXLLptIyKZ3PijHc/Ci521wygORCUCCred+E/twuqRyAkE02BAWPmsHOg==", - "requires": { - "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.4.2", - "semver": "^6.3.1" - }, - "dependencies": { - "semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" - } - } - }, - "babel-plugin-polyfill-corejs3": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.3.tgz", - "integrity": "sha512-z41XaniZL26WLrvjy7soabMXrfPWARN25PZoriDEiLMxAp50AUW3t35BGQUMg5xK3UrpVTtagIDklxYa+MhiNA==", - "requires": { - "@babel/helper-define-polyfill-provider": "^0.4.2", - "core-js-compat": "^3.31.0" - } - }, - "babel-plugin-polyfill-regenerator": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.2.tgz", - "integrity": "sha512-tAlOptU0Xj34V1Y2PNTL4Y0FOJMDB6bZmoW39FeCQIhigGLkqu3Fj6uiXpxIf6Ij274ENdYx64y6Au+ZKlb1IA==", - "requires": { - "@babel/helper-define-polyfill-provider": "^0.4.2" - } - }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -6209,51 +1995,11 @@ "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==" }, - "browserslist": { - "version": "4.21.10", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz", - "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==", - "requires": { - "caniuse-lite": "^1.0.30001517", - "electron-to-chromium": "^1.4.477", - "node-releases": "^2.0.13", - "update-browserslist-db": "^1.0.11" - } - }, - "buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" - }, - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - }, "camelcase": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==" }, - "caniuse-lite": { - "version": "1.0.30001534", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001534.tgz", - "integrity": "sha512-vlPVrhsCS7XaSh2VvWluIQEzVhefrUQcEsQWSS5A5V+dM07uv1qHeQzAOTGIMy9i3e9bH15+muvI/UHojVgS/Q==" - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, "cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -6279,16 +2025,6 @@ } } }, - "clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "requires": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - } - }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -6307,42 +2043,11 @@ "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" }, - "commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==" - }, - "commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==" - }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, - "convert-source-map": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", - "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", - "requires": { - "safe-buffer": "~5.1.1" - } - }, - "core-js": { - "version": "3.32.2", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.32.2.tgz", - "integrity": "sha512-pxXSw1mYZPDGvTQqEc5vgIb83jGQKFGYWY76z4a7weZXUolw3G+OvpZqSRcfYOoOVUQJYEPsWeQK8pKEnUtWxQ==" - }, - "core-js-compat": { - "version": "3.32.2", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.32.2.tgz", - "integrity": "sha512-+GjlguTDINOijtVRUxrQOv3kfu9rl+qPNdX2LTbJ/ZyVTuxK+ksVSAGX1nHstu4hrv1En/uPTtWgq2gI5wt4AQ==", - "requires": { - "browserslist": "^4.21.10" - } - }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -6366,26 +2071,6 @@ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==" }, - "define-data-property": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.0.tgz", - "integrity": "sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g==", - "requires": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" - } - }, - "define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "requires": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - } - }, "diff": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", @@ -6396,112 +2081,16 @@ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, - "electron-to-chromium": { - "version": "1.4.523", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.523.tgz", - "integrity": "sha512-9AreocSUWnzNtvLcbpng6N+GkXnCcBR80IQkxRC9Dfdyg4gaWNUPBujAHUpKkiUkoSoR9UlhA4zD/IgBklmhzg==" - }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, - "es-abstract": { - "version": "1.22.2", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.2.tgz", - "integrity": "sha512-YoxfFcDmhjOgWPWsV13+2RNjq1F6UQnfs+8TftwNqtzlmFzEXvlUwdrNrYeaizfjQzRMxkZ6ElWMOJIFKdVqwA==", - "requires": { - "array-buffer-byte-length": "^1.0.0", - "arraybuffer.prototype.slice": "^1.0.2", - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "es-set-tostringtag": "^2.0.1", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.1", - "get-symbol-description": "^1.0.0", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", - "has": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.5", - "is-array-buffer": "^3.0.2", - "is-callable": "^1.2.7", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.12", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.3", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "safe-array-concat": "^1.0.1", - "safe-regex-test": "^1.0.0", - "string.prototype.trim": "^1.2.8", - "string.prototype.trimend": "^1.0.7", - "string.prototype.trimstart": "^1.0.7", - "typed-array-buffer": "^1.0.0", - "typed-array-byte-length": "^1.0.0", - "typed-array-byte-offset": "^1.0.0", - "typed-array-length": "^1.0.4", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.11" - } - }, - "es-array-method-boxes-properly": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", - "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==" - }, - "es-set-tostringtag": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", - "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==", - "requires": { - "get-intrinsic": "^1.1.3", - "has": "^1.0.3", - "has-tostringtag": "^1.0.0" - } - }, - "es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } - }, "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" - }, - "find-cache-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", - "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", - "requires": { - "commondir": "^1.0.1", - "make-dir": "^2.0.0", - "pkg-dir": "^3.0.0" - } - }, "find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -6516,14 +2105,6 @@ "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==" }, - "for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "requires": { - "is-callable": "^1.1.3" - } - }, "foreground-child": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", @@ -6533,68 +2114,16 @@ "signal-exit": "^4.0.1" } }, - "fs-readdir-recursive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", - "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==" - }, "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "function.prototype.name": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", - "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "functions-have-names": "^1.2.3" - } - }, - "functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==" - }, - "gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "peer": true + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, - "get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" - } - }, - "get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", - "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - } - }, "glob": { "version": "10.3.4", "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.4.tgz", @@ -6625,84 +2154,16 @@ } } }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" - }, - "globalthis": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", - "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", - "requires": { - "define-properties": "^1.1.3" - } - }, - "gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "requires": { - "get-intrinsic": "^1.1.3" - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==" - }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, - "has-property-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", - "requires": { - "get-intrinsic": "^1.1.1" - } - }, - "has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==" - }, - "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" - }, - "has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "requires": { - "has-symbols": "^1.0.2" - } - }, "he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" }, - "homedir-polyfill": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", - "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", - "requires": { - "parse-passwd": "^1.0.0" - } - }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -6717,164 +2178,26 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, - "internal-slot": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", - "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", - "requires": { - "get-intrinsic": "^1.2.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" - } - }, - "is-array-buffer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", - "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", - "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", - "is-typed-array": "^1.1.10" - } - }, - "is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "requires": { - "has-bigints": "^1.0.1" - } - }, - "is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==" - }, - "is-core-module": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", - "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", - "requires": { - "has": "^1.0.3" - } - }, - "is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "requires": { - "has-tostringtag": "^1.0.0" - } - }, "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, - "is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==" - }, - "is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", - "requires": { - "has-tostringtag": "^1.0.0" - } - }, "is-plain-obj": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==" }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "requires": { - "isobject": "^3.0.1" - } - }, - "is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", - "requires": { - "call-bind": "^1.0.2" - } - }, - "is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "requires": { - "has-symbols": "^1.0.2" - } - }, - "is-typed-array": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", - "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", - "requires": { - "which-typed-array": "^1.1.11" - } - }, "is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==" }, - "is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "requires": { - "call-bind": "^1.0.2" - } - }, - "isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" - }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==" - }, "jackspeak": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.3.tgz", @@ -6884,11 +2207,6 @@ "@pkgjs/parseargs": "^0.11.0" } }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, "js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -6897,22 +2215,6 @@ "argparse": "^2.0.1" } }, - "jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==" - }, - "json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "peer": true - }, - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" - }, "locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -6921,11 +2223,6 @@ "p-locate": "^5.0.0" } }, - "lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" - }, "log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -6962,23 +2259,6 @@ } } }, - "lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "requires": { - "yallist": "^3.0.2" - } - }, - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" - } - }, "minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -7193,62 +2473,16 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "nan": { - "version": "2.18.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz", - "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==" + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.28.0.tgz", + "integrity": "sha512-fTsDz99OTq2sVePhGdp4qQhggZFtKr64ZNVyVajRKtMOkJxYekplBh577PiJB12v/D3s2E5cGtOI45LWp6rnLQ==", + "dev": true }, "nanoid": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==" }, - "node-environment-flags": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.6.tgz", - "integrity": "sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw==", - "requires": { - "object.getownpropertydescriptors": "^2.0.3", - "semver": "^5.7.0" - } - }, - "node-releases": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", - "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==" - }, - "object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==" - }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" - }, - "object.assign": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", - "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - } - }, - "object.getownpropertydescriptors": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.7.tgz", - "integrity": "sha512-PrJz0C2xJ58FNn11XV2lr4Jt5Gzl94qpy9Lu0JlfEj14z88sqbSBJCBEzdlNUCzY2gburhbrwOZ5BHCmuNUy0g==", - "requires": { - "array.prototype.reduce": "^1.0.6", - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "safe-array-concat": "^1.0.0" - } - }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -7273,16 +2507,6 @@ "p-limit": "^3.0.2" } }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" - }, - "parse-passwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", - "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==" - }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -7298,11 +2522,6 @@ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" }, - "path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" - }, "path-scurry": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", @@ -7319,74 +2538,11 @@ } } }, - "picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" - }, "picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" - }, - "pirates": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", - "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==" - }, - "pkg-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", - "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", - "requires": { - "find-up": "^3.0.0" - }, - "dependencies": { - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "requires": { - "locate-path": "^3.0.0" - } - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "requires": { - "p-limit": "^2.0.0" - } - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==" - } - } - }, "prettier": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz", @@ -7400,78 +2556,11 @@ "safe-buffer": "^5.1.0" } }, - "regenerate": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==" - }, - "regenerate-unicode-properties": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", - "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", - "requires": { - "regenerate": "^1.4.2" - } - }, - "regenerator-runtime": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", - "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" - }, - "regenerator-transform": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", - "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", - "requires": { - "@babel/runtime": "^7.8.4" - } - }, - "regexp.prototype.flags": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", - "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "set-function-name": "^2.0.0" - } - }, - "regexpu-core": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", - "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", - "requires": { - "@babel/regjsgen": "^0.8.0", - "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.1.0", - "regjsparser": "^0.9.1", - "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.1.0" - } - }, - "regjsparser": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", - "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", - "requires": { - "jsesc": "~0.5.0" - } - }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" }, - "resolve": { - "version": "1.22.6", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz", - "integrity": "sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==", - "requires": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } - }, "rimraf": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", @@ -7495,37 +2584,11 @@ } } }, - "safe-array-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", - "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==", - "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", - "has-symbols": "^1.0.3", - "isarray": "^2.0.5" - } - }, "safe-buffer": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" }, - "safe-regex-test": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", - "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", - "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "is-regex": "^1.1.4" - } - }, - "semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==" - }, "serialize-javascript": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", @@ -7534,24 +2597,6 @@ "randombytes": "^2.1.0" } }, - "set-function-name": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", - "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", - "requires": { - "define-data-property": "^1.0.1", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.0" - } - }, - "shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "requires": { - "kind-of": "^6.0.2" - } - }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -7565,43 +2610,11 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" }, - "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - } - }, "signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==" }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "peer": true - }, - "source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - } - } - }, "string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -7652,36 +2665,6 @@ } } }, - "string.prototype.trim": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", - "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - } - }, - "string.prototype.trimend": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", - "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - } - }, - "string.prototype.trimstart": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", - "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - } - }, "strip-ansi": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", @@ -7710,26 +2693,6 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - }, - "dependencies": { - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" - } - } - }, - "supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" - }, "temp": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", @@ -7754,100 +2717,39 @@ } } }, - "typed-array-buffer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", - "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", - "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", - "is-typed-array": "^1.1.10" - } - }, - "typed-array-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", - "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", - "requires": { - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" - } - }, - "typed-array-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", - "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", - "requires": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" - } - }, - "typed-array-length": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", - "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", - "requires": { - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "is-typed-array": "^1.1.9" - } - }, - "unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", - "requires": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - } - }, - "unicode-canonical-property-names-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", - "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==" - }, - "unicode-match-property-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", - "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", - "requires": { - "unicode-canonical-property-names-ecmascript": "^2.0.0", - "unicode-property-aliases-ecmascript": "^2.0.0" - } - }, - "unicode-match-property-value-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", - "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==" - }, - "unicode-property-aliases-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", - "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==" - }, - "update-browserslist-db": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", - "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", - "requires": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" - } - }, - "v8flags": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", - "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==", - "requires": { - "homedir-polyfill": "^1.0.1" - } + "typescript": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "dev": true, + "requires": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" + } + }, + "undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true }, "which": { "version": "2.0.2", @@ -7857,30 +2759,6 @@ "isexe": "^2.0.0" } }, - "which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "requires": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - } - }, - "which-typed-array": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", - "integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==", - "requires": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" - } - }, "workerpool": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", @@ -7962,11 +2840,6 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" }, - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" - }, "yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", diff --git a/package.json b/package.json index 3448ce7b..97aec8d9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "echojs", - "version": "0.0.0", + "version": "0.2.0", "description": "Compile ES6 to native code", "main": "ejs.js", "bin": { @@ -51,14 +51,15 @@ "homepage": "https://github.com/toshok/echojs", "private": true, "dependencies": { - "@babel/cli": "^7.22.15", - "@babel/node": "^7.22.19", - "@babel/preset-env": "^7.22.20", "colors": "^1.4.0", "glob": "^10.3.4", "mocha": "^10.2.0", - "nan": "^2.18.0", "prettier": "^3.0.3", "temp": "^0.9.4" + }, + "devDependencies": { + "@types/node": "^26.1.1", + "nan": "^2.28.0", + "typescript": "^7.0.2" } } diff --git a/packaging/.gitignore b/packaging/.gitignore deleted file mode 100644 index da2a2843..00000000 --- a/packaging/.gitignore +++ /dev/null @@ -1 +0,0 @@ -npm-tmp diff --git a/packaging/Makefile b/packaging/Makefile deleted file mode 100644 index 6afd914d..00000000 --- a/packaging/Makefile +++ /dev/null @@ -1,93 +0,0 @@ -TOP=.. -include $(TOP)/build/config.mk - -PWD:=$(shell pwd) - -.NOTPARALLEL: - -#INSTALLKBYTES:=$(shell du -sk $(DIST_ROOT) | cut -f 1) -#NUMFILES=$(shell find $(DIST_ROOT) | wc -l) - -# arg1 = input directory -# arg2 = output path -define make_archive - @echo [ARCHIVE] `basename $2` - @(cd $1; find . -print0 | pax -w -0 -x cpio 2>/dev/null | gzip -9 > $2) -endef - -# arg1 = input directory -# arg2 = output path -define mkbom - @echo [MKBOM] $2 - @mkbom $1 $2 -endef - -# arg1 = packagename-version -define mkpkg - @echo [PKG] $1.pkg - @(cd pkgtmp; xar -z --no-compress .*Payload --no-compress .*Scripts -c -f ../$1.pkg * && echo made $(TOP)/packaging/$1.pkg) -endef - -release: - @rm -rf pkgtmp Scripts - @mkdir -p pkgtmp/Resources/en.lproj - @mkdir -p pkgtmp/$(PRODUCT_name).pkg - $(call rewrite, package-template/Resources/en.lproj/Readme.rtf.in, pkgtmp/Resources/en.lproj/Readme) - $(call rewrite, package-template/Resources/en.lproj/License.rtf.in, pkgtmp/Resources/en.lproj/License) - $(call rewrite, package-template/Distribution.in, pkgtmp/Distribution) - $(call rewrite, package-template/product.pkg/PackageInfo.in, pkgtmp/$(PRODUCT_name).pkg/PackageInfo) - $(call mkbom,$(DIST_ROOT),pkgtmp/$(PRODUCT_name).pkg/Bom) - @mkdir Scripts - $(call rewrite, package-template/Scripts/postinstall.in, Scripts/postinstall) - @chmod a+x Scripts/postinstall - $(call make_archive,Scripts,$(PWD)/pkgtmp/$(PRODUCT_name).pkg/Scripts) - @rm -r Scripts - $(call make_archive,$(DIST_ROOT),$(PWD)/pkgtmp/$(PRODUCT_name).pkg/Payload) - $(call mkpkg,$(PRODUCT_NAME)-$(PRODUCT_VERSION)) - -npmtmp=npm-tmp -bindir=$(npmtmp)/bin -libdir=$(npmtmp)/lib -includedir=$(npmtmp)/include -libarchdir_osx=$(npmtmp)/lib/x86_64-darwin -libarchdir_sim=$(npmtmp)/lib/x86-darwin -libarchdir_dev=$(npmtmp)/lib/arm-darwin -npm-release-dist: - @rm -rf $(npmtmp) - @rm -f $(npmtmp)/package.json - @mkdir -p $(bindir) - @mkdir -p $(libdir) - @mkdir -p $(libarchdir_osx) - @mkdir -p $(libarchdir_sim) - @mkdir -p $(libarchdir_dev) - @mkdir -p $(npmtmp)/include - $(call rewrite, npm/package.json.in, $(npmtmp)/package.json) - @cp $(TOP)/ejs.exe $(bindir)/ejs - @cp $(TOP)/runtime/*.h $(includedir) - @cp $(TOP)/runtime/libecho.a $(libarchdir_osx) - @cp $(TOP)/external-deps/pcre-osx/.libs/libpcre16.a $(libarchdir_osx) - @cp $(TOP)/external-deps/double-conversion-osx/double-conversion/libdouble-conversion.a $(libarchdir_osx) - @cp $(TOP)/runtime/libecho.a.sim $(libarchdir_sim)/libecho.a - @cp $(TOP)/external-deps/pcre-iossim/.libs/libpcre16.a $(libarchdir_sim) - @cp $(TOP)/external-deps/double-conversion-iossim/double-conversion/libdouble-conversion.a $(libarchdir_sim) - @cp $(TOP)/runtime/libecho.a.armv7 $(libarchdir_dev)/libecho.a - @cp $(TOP)/external-deps/pcre-iosdev/.libs/libpcre16.a $(libarchdir_dev) - @cp $(TOP)/external-deps/double-conversion-iosdev/double-conversion/libdouble-conversion.a $(libarchdir_dev) - @cp $(TOP)/modules/objc_internal/objc_internal.ejs $(libdir) - @cp $(TOP)/node-compat/node-compat.ejs $(libdir) - @cp $(TOP)/node-compat/libejsnodecompat-module.a $(libarchdir_osx) - @cp $(TOP)/node-compat/libejsnodecompat-module.a.sim $(libarchdir_sim) - @cp $(TOP)/node-compat/libejsnodecompat-module.a.armv7 $(libarchdir_dev) - - - - -# gross, but for now strip the .a's we've installed so that we aren't passing debug/local symbols around... -dist-local:: - strip -S -x -X $(DIST_ROOT)/usr/lib/$(LIBCOFFEEKIT_A) - strip -S -x -X $(DIST_ROOT)/usr/lib/libjs_static.a - -clean-local:: - @rm -rf pkgtmp Scripts - -include $(TOP)/build/build.mk diff --git a/packaging/homebrew/echojs.rb.in b/packaging/homebrew/echojs.rb.in new file mode 100644 index 00000000..064b1936 --- /dev/null +++ b/packaging/homebrew/echojs.rb.in @@ -0,0 +1,39 @@ +# Homebrew formula template for echojs (release-P2). Generated by +# make-formula.sh from a //:dist tarball, which fills the url, sha256, +# version, and llvm major from the tarball and its dist-info. +# +# The keg keeps the whole relocatable layout together under libexec +# (the driver resolves include/ and lib/ relative to its own binary and +# does not chase symlinks, so a brew link farm is the wrong shape); bin +# gets an absolute-path exec shim instead. +class Echojs < Formula + desc "Ahead-of-time compiler for JavaScript" + homepage "https://github.com/toshok/echojs" + url "@URL@" + sha256 "@SHA256@" + version "@VERSION@" + license "MIT" + + depends_on arch: :arm64 + # keg-only llvm; the driver discovers #{HOMEBREW_PREFIX}/opt/llvm@N/bin + # on its own and refuses to run against a different major + depends_on "llvm@@LLVM_MAJOR@" + + def install + rm_f "install.sh" # the tarball's prefix installer; brew owns install here + libexec.install Dir["*"] + bin.write_exec_script libexec/"bin/ejs" + end + + test do + (testpath/"hello.js").write <<~EOS + class Greeter { + constructor(who) { this.who = who; } + greet() { return `hello, ${this.who}`; } + } + console.log(new Greeter("brew").greet()); + EOS + system bin/"ejs", "-q", "-o", "hello.exe", "hello.js" + assert_equal "hello, brew", shell_output("./hello.exe").strip + end +end diff --git a/packaging/homebrew/make-formula.sh b/packaging/homebrew/make-formula.sh new file mode 100755 index 00000000..2c0b0145 --- /dev/null +++ b/packaging/homebrew/make-formula.sh @@ -0,0 +1,57 @@ +#!/bin/sh +# Generate the echojs Homebrew formula from a //:dist tarball +# (release-P2). Until release-P3 hosts tagged release artifacts the +# default URL is the local tarball itself (file://…), which is enough +# for `brew install --formula echojs.rb` and CI smoke tests; pass the +# hosted URL once one exists: +# +# make-formula.sh --tarball dist-out/echojs-*.tar.gz \ +# [--url https://github.com/toshok/echojs/releases/download/vX/…] \ +# [--out echojs.rb] +set -eu + +TARBALL= URL= OUT=echojs.rb + +usage() { + echo "usage: $0 --tarball PATH [--url URL] [--out PATH]" >&2 + exit 1 +} + +while [ $# -gt 0 ]; do + case "$1" in + --tarball) [ $# -ge 2 ] || usage; TARBALL="$2"; shift 2 ;; + --url) [ $# -ge 2 ] || usage; URL="$2"; shift 2 ;; + --out) [ $# -ge 2 ] || usage; OUT="$2"; shift 2 ;; + *) usage ;; + esac +done +[ -n "$TARBALL" ] && [ -f "$TARBALL" ] || usage + +HERE="$(cd "$(dirname "$0")" && pwd)" +ABS_TARBALL="$(cd "$(dirname "$TARBALL")" && pwd)/$(basename "$TARBALL")" +[ -n "$URL" ] || URL="file://$ABS_TARBALL" + +# dist-info lives at /dist-info inside the tarball; the exact +# member path avoids needing GNU tar's --wildcards +NAME="$(basename "$TARBALL" .tar.gz)" +INFO="$(tar -xzOf "$ABS_TARBALL" "$NAME/dist-info")" +eval "$INFO" # EJS_VERSION, EJS_TRIPLE, EJS_SHORT_TRIPLE, EJS_OS, EJS_LLVM_MAJOR + +if [ "$EJS_OS" != macos ]; then + echo "error: the homebrew formula wants a macos dist tarball (got EJS_OS=$EJS_OS)" >&2 + exit 1 +fi + +if command -v shasum >/dev/null 2>&1; then + SHA256="$(shasum -a 256 "$ABS_TARBALL" | cut -d' ' -f1)" +else + SHA256="$(sha256sum "$ABS_TARBALL" | cut -d' ' -f1)" +fi + +sed -e "s|@URL@|$URL|" \ + -e "s|@SHA256@|$SHA256|" \ + -e "s|@VERSION@|$EJS_VERSION|" \ + -e "s|@LLVM_MAJOR@|$EJS_LLVM_MAJOR|" \ + "$HERE/echojs.rb.in" > "$OUT" + +echo "wrote $OUT (version $EJS_VERSION, llvm@$EJS_LLVM_MAJOR, $URL)" diff --git a/packaging/install.sh b/packaging/install.sh new file mode 100755 index 00000000..61d9cf55 --- /dev/null +++ b/packaging/install.sh @@ -0,0 +1,119 @@ +#!/bin/sh +# echojs prefix installer (release-P2). Ships at the root of the dist +# tarball; run it from the unpacked directory: +# +# tar xzf echojs--.tar.gz +# cd echojs-- +# sudo ./install.sh # into /usr/local +# ./install.sh --prefix ~/.local # anywhere writable +# +# The tree is copied whole to $PREFIX/lib/echojs/ (the driver +# resolves include/ and lib/ relative to its own binary, so bin/, +# include/ and lib/ must stay together) and $PREFIX/bin/ejs becomes a +# tiny exec shim holding the absolute path — the driver does not chase +# symlinks, so a symlink would break that resolution. +# +# ./install.sh --uninstall [--prefix P] removes both again +set -eu + +PREFIX=/usr/local +UNINSTALL=no + +usage() { + echo "usage: $0 [--prefix PREFIX] [--uninstall]" >&2 + exit 1 +} + +while [ $# -gt 0 ]; do + case "$1" in + --prefix) [ $# -ge 2 ] || usage; PREFIX="$2"; shift 2 ;; + --prefix=*) PREFIX="${1#--prefix=}"; shift ;; + --uninstall) UNINSTALL=yes; shift ;; + -h|--help) usage ;; + *) usage ;; + esac +done + +HERE="$(cd "$(dirname "$0")" && pwd)" +[ -f "$HERE/dist-info" ] || { + echo "error: $HERE/dist-info not found — run this from an unpacked echojs dist directory" >&2 + exit 1 +} +# EJS_VERSION, EJS_TRIPLE, EJS_SHORT_TRIPLE, EJS_OS, EJS_LLVM_MAJOR +. "$HERE/dist-info" + +NAME="echojs-$EJS_VERSION-$EJS_SHORT_TRIPLE" +DEST="$PREFIX/lib/echojs/$NAME" +SHIM="$PREFIX/bin/ejs" + +if [ "$UNINSTALL" = yes ]; then + rm -rf "$DEST" + rmdir "$PREFIX/lib/echojs" 2>/dev/null || true + # only remove the shim if it is ours (points into $DEST) + if [ -f "$SHIM" ] && grep -q "lib/echojs/$NAME/bin/ejs" "$SHIM" 2>/dev/null; then + rm -f "$SHIM" + fi + echo "uninstalled $NAME from $PREFIX" + exit 0 +fi + +mkdir -p "$PREFIX/bin" "$PREFIX/lib/echojs" +rm -rf "$DEST" +cp -R "$HERE" "$DEST" +rm -f "$DEST/install.sh" + +cat > "$SHIM" </dev/null | grep -q "LLVM version $EJS_LLVM_MAJOR\."; then + echo "$d" + return 0 + fi + done + if opt --version 2>/dev/null | grep -q "LLVM version $EJS_LLVM_MAJOR\."; then + echo "(PATH)" + return 0 + fi + return 1 +} + +if BINDIR="$(find_opt)"; then + echo " llvm $EJS_LLVM_MAJOR: $BINDIR" +else + echo "warning: no LLVM $EJS_LLVM_MAJOR opt/llc found in the conventional locations." >&2 + if [ "$EJS_OS" = macos ]; then + echo " install it with: brew install llvm" >&2 + else + echo " install it from https://apt.llvm.org (or your distribution's llvm-$EJS_LLVM_MAJOR packages)" >&2 + fi + echo " or point LLVM_BINDIR at a bindir containing a matching opt/llc." >&2 +fi + +if [ "$EJS_OS" = linux ] && command -v ldconfig >/dev/null 2>&1; then + # the trailing space matters: "libuv.so " is the -dev symlink the + # final link needs; "libuv.so.1" is just the runtime library + for spec in libuv:libuv1-dev libunwind:libunwind-dev; do + lib="${spec%%:*}"; pkg="${spec#*:}" + if ! ldconfig -p 2>/dev/null | grep -q "$lib\.so "; then + echo "warning: $lib development package not found (compiled programs link against it)" >&2 + echo " e.g. apt install $pkg" >&2 + fi + done +fi diff --git a/packaging/npm/.gitignore b/packaging/npm/.gitignore new file mode 100644 index 00000000..3be4a7b0 --- /dev/null +++ b/packaging/npm/.gitignore @@ -0,0 +1,2 @@ +dist/ +*.tgz diff --git a/packaging/npm/README.md b/packaging/npm/README.md new file mode 100644 index 00000000..d582ce3f --- /dev/null +++ b/packaging/npm/README.md @@ -0,0 +1,16 @@ +# echojs (npm wrapper) + +An ahead-of-time compiler for JavaScript. This package downloads the +platform's prebuilt echojs toolchain at install time (macOS arm64, +Linux arm64/x86_64) and exposes its `ejs` driver on your PATH. + + npm install -g @pirouette/echojs + ejs -o hello hello.js && ./hello + +Compiling needs an LLVM toolchain with the major version the release +was built against (`ejs` discovers it and fails loudly otherwise — +macOS: `brew install llvm`; Linux: https://apt.llvm.org, plus libuv and +libunwind development packages for linking). + +`EJS_NPM_TARBALL=/path/to/echojs--.tar.gz` makes the +install use a local dist tarball instead of downloading. diff --git a/packaging/npm/bin/ejs.js b/packaging/npm/bin/ejs.js new file mode 100644 index 00000000..cab1cddd --- /dev/null +++ b/packaging/npm/bin/ejs.js @@ -0,0 +1,25 @@ +#!/usr/bin/env node +// Exec shim for the npm wrapper (release-P2). node realpaths the main +// module, so __dirname is the package's true location even when npm +// invokes this through the node_modules/.bin symlink — and the driver +// binary therefore sees an argv[0] it can resolve include/ and lib/ +// against (it does not chase symlinks itself). +"use strict"; + +const path = require("path"); +const fs = require("fs"); +const { spawnSync } = require("child_process"); + +const exe = path.join(__dirname, "..", "dist", "bin", "ejs"); +if (!fs.existsSync(exe)) { + console.error("ejs: native toolchain missing — the echojs postinstall did not run or failed;"); + console.error(" reinstall the package (npm rebuild echojs) and check its output."); + process.exit(1); +} + +const r = spawnSync(exe, process.argv.slice(2), { stdio: "inherit" }); +if (r.error) { + console.error(`ejs: failed to run ${exe}: ${r.error.message}`); + process.exit(1); +} +process.exit(r.status === null ? 1 : r.status); diff --git a/packaging/npm/install.js b/packaging/npm/install.js new file mode 100644 index 00000000..cce92271 --- /dev/null +++ b/packaging/npm/install.js @@ -0,0 +1,92 @@ +// npm postinstall (release-P2): fetch the platform dist tarball and +// unpack it as ./dist, which bin/ejs.js execs out of. The wrapper's +// version pins the release tag: v must have uploaded +// echojs--.tar.gz assets (release-P3 automation +// owns making that true). +// +// EJS_NPM_TARBALL=/path/to/echojs-*.tar.gz overrides the download — +// the pre-release/CI path, and the escape hatch for offline installs. +"use strict"; + +const fs = require("fs"); +const os = require("os"); +const path = require("path"); +const { spawnSync } = require("child_process"); + +const version = require("./package.json").version; + +const SHORT_TRIPLES = { + "darwin-arm64": "arm64-macos", + "linux-arm64": "arm64-linux", + "linux-x64": "x86_64-linux", +}; + +function fail(msg) { + console.error(`echojs install: ${msg}`); + process.exit(1); +} + +async function main() { + const key = `${process.platform}-${process.arch}`; + const shortTriple = SHORT_TRIPLES[key]; + if (!shortTriple) { + fail( + `no prebuilt echojs for ${key} (supported: ${Object.keys(SHORT_TRIPLES).join(", ")})` + ); + } + + const work = fs.mkdtempSync(path.join(os.tmpdir(), "echojs-npm-")); + let tarball = process.env["EJS_NPM_TARBALL"]; + if (tarball) { + // postinstall runs with cwd = the package dir; a relative + // override means relative to where `npm install` was invoked + tarball = path.resolve(process.env["INIT_CWD"] || process.cwd(), tarball); + if (!fs.existsSync(tarball)) fail(`EJS_NPM_TARBALL=${tarball} does not exist`); + console.log(`echojs install: using ${tarball}`); + } else { + const url = `https://github.com/toshok/echojs/releases/download/v${version}/echojs-${version}-${shortTriple}.tar.gz`; + console.log(`echojs install: fetching ${url}`); + const res = await fetch(url); + if (!res.ok) { + fail( + `download failed (${res.status} ${res.statusText}); ` + + `if you are offline or the release is missing, point EJS_NPM_TARBALL at a local tarball` + ); + } + tarball = path.join(work, "dist.tar.gz"); + fs.writeFileSync(tarball, Buffer.from(await res.arrayBuffer())); + } + + const unpack = path.join(work, "unpack"); + fs.mkdirSync(unpack); + const tar = spawnSync("tar", ["-xzf", tarball, "-C", unpack], { stdio: "inherit" }); + if (tar.status !== 0) fail(`tar extraction failed (${tar.status ?? tar.error})`); + + const entries = fs.readdirSync(unpack).filter((e) => e.startsWith("echojs-")); + if (entries.length !== 1) fail(`expected one echojs-* directory in the tarball, got [${entries}]`); + + const dist = path.join(__dirname, "dist"); + fs.rmSync(dist, { recursive: true, force: true }); + fs.renameSync(path.join(unpack, entries[0]), dist); + fs.rmSync(work, { recursive: true, force: true }); + + const exe = path.join(dist, "bin", "ejs"); + if (!fs.existsSync(exe)) fail(`unpacked tarball has no bin/ejs`); + fs.chmodSync(exe, 0o755); + + // dist-info records what the driver needs on top of this package + // (a matching LLVM major); surface it once at install time + const info = path.join(dist, "dist-info"); + const major = fs.existsSync(info) + ? (fs.readFileSync(info, "utf8").match(/^EJS_LLVM_MAJOR=(\d+)$/m) || [])[1] + : undefined; + console.log(`echojs install: ${entries[0]} ready`); + if (major) { + console.log( + `echojs install: compiling needs LLVM ${major} (opt/llc) — ` + + `brew install llvm / https://apt.llvm.org; ejs verifies and fails loudly otherwise` + ); + } +} + +main().catch((e) => fail(e.stack || String(e))); diff --git a/packaging/npm/package.json b/packaging/npm/package.json new file mode 100644 index 00000000..160847e2 --- /dev/null +++ b/packaging/npm/package.json @@ -0,0 +1,38 @@ +{ + "name": "@pirouette/echojs", + "version": "0.2.0", + "description": "Ahead-of-time compiler for JavaScript — native toolchain wrapper", + "bin": { + "ejs": "bin/ejs.js" + }, + "scripts": { + "postinstall": "node install.js" + }, + "files": [ + "bin", + "install.js", + "README.md" + ], + "os": [ + "darwin", + "linux" + ], + "cpu": [ + "arm64", + "x64" + ], + "engines": { + "node": ">=18" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/toshok/echojs.git", + "directory": "packaging/npm" + }, + "publishConfig": { + "access": "public" + }, + "author": "Chris Toshok ", + "license": "MIT", + "homepage": "https://github.com/toshok/echojs#readme" +} diff --git a/packaging/npm/package.json.in b/packaging/npm/package.json.in deleted file mode 100644 index a9e6aca5..00000000 --- a/packaging/npm/package.json.in +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "pirouette-toolchain-darwin-x64", - "version": "@PRODUCT_VERSION@", - "description": "Compile ES2015 to native code on OSX/iOS/linux", - "main": "index.js", - "files": [ - "bin", - "include", - "lib" - ], - "repository": { - "type": "git", - "url": "@PRODUCT_GITHUB_URL@" - }, - "author": "Chris Toshok <@PRODUCT_EMAIL@> (https://blog.toshokelectric.com/)", - "license": "MIT", - "bugs": { - "url": "@PRODUCT_GITHUB_URL@/issues" - }, - "homepage": "@PRODUCT_GITHUB_URL@", - "readme": "@PRODUCT_GITHUB_URL@#readme" -} diff --git a/packaging/prepare-release.sh b/packaging/prepare-release.sh new file mode 100755 index 00000000..7c2223ca --- /dev/null +++ b/packaging/prepare-release.sh @@ -0,0 +1,74 @@ +#!/bin/sh +# Cut a release locally (release-P3): roll the CHANGELOG, stamp the +# version everywhere it lives, commit, and tag. Nothing is pushed — +# pushing the tag is the action that runs the release pipeline +# (.github/workflows/release.yml), so that stays a human decision: +# +# ./packaging/prepare-release.sh 0.2.0 +# git push origin HEAD "v0.2.0" +# +# The version lives in exactly two files — package.json (the dist +# tarball's source of truth, read by buck-dist.sh) and +# packaging/npm/package.json (the wrapper, whose version pins the +# release tag its postinstall downloads from) — plus the tag itself; +# release.yml's version-check job refuses a tag where they disagree. +set -eu + +VERSION="${1:-}" +case "$VERSION" in + *[!0-9.]*|"") echo "usage: $0 " >&2; exit 1 ;; +esac +echo "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$' || { + echo "error: '$VERSION' is not a major.minor.patch version" >&2 + exit 1 +} + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$ROOT" + +test -z "$(git status --porcelain)" || { + echo "error: working tree not clean" >&2 + exit 1 +} +! git rev-parse -q --verify "refs/tags/v$VERSION" > /dev/null || { + echo "error: tag v$VERSION already exists" >&2 + exit 1 +} + +# the Unreleased section must exist and have content — an empty +# changelog entry means the release story hasn't been written +grep -q '^## \[Unreleased\]' CHANGELOG.md || { + echo "error: CHANGELOG.md has no '## [Unreleased]' section" >&2 + exit 1 +} +BODY="$(awk '/^## \[Unreleased\]/{f=1; next} /^## /{f=0} f' CHANGELOG.md | grep -cv '^[[:space:]]*$' || true)" +[ "$BODY" -gt 0 ] || { + echo "error: the Unreleased section of CHANGELOG.md is empty — write the release notes first" >&2 + exit 1 +} + +TODAY="$(date +%Y-%m-%d)" +awk -v v="$VERSION" -v d="$TODAY" ' + /^## \[Unreleased\]$/ { print; print ""; print "## [" v "] - " d; next } + { print } +' CHANGELOG.md > CHANGELOG.md.new +mv CHANGELOG.md.new CHANGELOG.md + +# npm stamps package.json (and the lockfile's mirrored version) in +# place; --allow-same-version because the tree may already carry the +# to-be-released version (it has since 0.2.0 was pre-stamped) +npm version --no-git-tag-version --allow-same-version "$VERSION" > /dev/null +(cd packaging/npm && npm version --no-git-tag-version --allow-same-version "$VERSION" > /dev/null) + +git add CHANGELOG.md package.json package-lock.json packaging/npm/package.json +git commit -q -m "release: v$VERSION" +git tag -a "v$VERSION" -m "echojs $VERSION" + +echo "prepared v$VERSION:" +git --no-pager log --oneline -1 +echo +echo "next:" +echo " git push origin HEAD \"v$VERSION\" # runs the release pipeline" +echo "the pipeline drafts the GitHub release; publishing it (and the" +echo "OIDC npm publish / tap push, if configured) is described in" +echo "docs/release-p3-results.md" diff --git a/prelude b/prelude new file mode 160000 index 00000000..023cf61f --- /dev/null +++ b/prelude @@ -0,0 +1 @@ +Subproject commit 023cf61fbeb5d5208ec32731f7869b4b7a853613 diff --git a/release/.gitignore b/release/.gitignore deleted file mode 100644 index b2532e40..00000000 --- a/release/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -release-readme.md -echojs-* diff --git a/release/Makefile b/release/Makefile deleted file mode 100644 index 8b02c9c8..00000000 --- a/release/Makefile +++ /dev/null @@ -1,37 +0,0 @@ -TOP=.. - -include $(TOP)/build/config.mk - -TARBALL_DIR=$(PRODUCT_name)-$(PRODUCT_VERSION) -libdir=$(TARBALL_DIR)/lib -archlibdir=$(TARBALL_DIR)/lib/darwin-x86-64 -includedir=$(TARBALL_DIR)/include/runtime -bindir=$(TARBALL_DIR)/bin -sampledir=$(TARBALL_DIR)/samples - -osx-tarball: osx-tarball-deps - rm -f $(TARBALL_DIR).tar.bz2 - rm -rf $(TARBALL_DIR) - mkdir -p $(bindir) - mkdir -p $(archlibdir) - mkdir -p $(includedir) - mkdir -p $(sampledir) - cp release-readme.md $(TARBALL_DIR)/README.md - cp $(TOP)/ejs.exe $(bindir)/ejs - cp $(TOP)/runtime/*.h $(includedir) - cp $(TOP)/runtime/libecho.a $(archlibdir) - cp $(TOP)/external-deps/pcre-osx/.libs/libpcre16.a $(archlibdir) - cp $(TOP)/node-compat/node-compat.ejs $(libdir) - cp $(TOP)/node-compat/libejsnodecompat-module.a $(archlibdir) - cp $(TOP)/ejs-llvm/ejs-llvm.ejs $(libdir) - cp $(TOP)/ejs-llvm/libejsllvm-module.a $(archlibdir) - cp $(TOP)/test/fetch.js $(sampledir) - tar -cvzf $(TARBALL_DIR).tar.gz $(TARBALL_DIR) - -osx-tarball-deps: release-readme.md - $(MAKE) -C .. all bootstrap - -release-readme.md: release-readme.md.in - @echo [gen] $< && sed -e "s,@PRODUCT_VERSION@,$(PRODUCT_VERSION)," $< > $@ - -include $(TOP)/build/build.mk diff --git a/release/release-readme.md.in b/release/release-readme.md.in deleted file mode 100644 index 45de1db7..00000000 --- a/release/release-readme.md.in +++ /dev/null @@ -1,18 +0,0 @@ -EchoJS @PRODUCT_VERSION@ ------------------------- - -OSX only test tarball - -you need llvm34 installed. - -```sh -$ brew install llvm34 -$ export LLVM_SUFFIX-3.4 -``` - -as an example of XmlHTTPRequest + ES6 Promises: - -```sh -$ bin/ejs samples/fetch.js -$ samples/fetch.js.exe http://www.google.com/ -``` diff --git a/release/trusty64/.gitignore b/release/trusty64/.gitignore deleted file mode 100644 index 8000dd9d..00000000 --- a/release/trusty64/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.vagrant diff --git a/release/trusty64/Vagrantfile b/release/trusty64/Vagrantfile deleted file mode 100644 index 3212c933..00000000 --- a/release/trusty64/Vagrantfile +++ /dev/null @@ -1,22 +0,0 @@ -# -*- mode: ruby -*- -# vi: set ft=ruby : - -# Vagrantfile API/syntax version. Don't touch unless you know what you're doing! -VAGRANTFILE_API_VERSION = "2" - -Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| - # All Vagrant configuration is done here. The most common configuration - # options are documented and commented below. For a complete reference, - # please see the online documentation at vagrantup.com. - - # Every Vagrant virtual environment requires a box to build off of. - config.vm.box = "ubuntu/trusty64" - config.vm.provision :shell, path: "provision.sh" - - config.vm.provider "virtualbox" do |v| - v.memory = 8192 - v.cpus = 2 - end - - config.vm.synced_folder "../../", "/src/echo-js" -end diff --git a/release/trusty64/provision.sh b/release/trusty64/provision.sh deleted file mode 100644 index 9da11e15..00000000 --- a/release/trusty64/provision.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh - -apt-get update -apt-get install -y llvm-3.4 clang-3.4 make git nodejs-legacy node-gyp npm libunwind8-dev libuv-dev build-essential dh-make bzr-builddeb -npm install -g coffee-script -npm install -g mocha diff --git a/runtime/BUCK b/runtime/BUCK new file mode 100644 index 00000000..de148c24 --- /dev/null +++ b/runtime/BUCK @@ -0,0 +1,197 @@ +load("//:defs.bzl", "EJS_COMPILER_FLAGS", "LLC_MTRIPLE", "llvm_bin") + +# gen-atoms.ts compiled to node-runnable JS (typescript comes from the +# repo's node_modules, untracked by buck2 — same treatment as the +# compiler's tsc steps in //lib) +genrule( + name = "gen-atoms-js", + srcs = ["gen-atoms.ts"], + out = "gen-atoms.js", + cmd = 'REPO="${TMP%%/buck-out/*}"; node "$REPO/node_modules/typescript/bin/tsc" ' + + "--ignoreConfig --strict --noUncheckedIndexedAccess --noImplicitOverride " + + "--noEmitOnError --target es2016 --module commonjs " + + '--types node --typeRoots "$REPO/node_modules/@types" ' + + '--outDir "${OUT%/*}" $SRCDIR/gen-atoms.ts', + visibility = ["PUBLIC"], +) + +genrule( + name = "atoms", + srcs = [ + "ejs-atoms.h", + ":gen-atoms-js", + ], + out = "ejs-atoms-gen.c", + # gen-atoms emits definitions only; add the includes it needs to + # compile as a standalone translation unit + cmd = "{ printf '#include \"ejs.h\"\\n#include \"ejs-value.h\"\\n#include \"ejs-string.h\"\\n\\n'; " + + "node $(location :gen-atoms-js) $SRCDIR/ejs-atoms.h; } > $OUT", +) + +genrule( + name = "webgl-constants-sorted", + srcs = ["ejs-webgl-constants.h"], + out = "ejs-webgl-constants-sorted.h", + cmd = "grep WEBGL_CONSTANT $SRCDIR/ejs-webgl-constants.h | sort > $OUT", +) + +# The invoke-closure trampoline is hand-written IR; llc it to an object +# that gets appended into libecho.a when the --srcdir tree is staged +# (see //:srcdir-tree). +genrule( + name = "platform-icc-o", + srcs = ["ejs-invoke-closure-catch.ll"], + out = "ejs-invoke-closure-catch.o", + cmd = llvm_bin("llc") + " -mtriple=" + LLC_MTRIPLE + + " --relocation-model=pic -filetype=obj -O2 -o $OUT $SRCDIR/ejs-invoke-closure-catch.ll", + visibility = ["PUBLIC"], +) + +# Staged as runtime/ in the --srcdir tree; the compiler passes -I runtime +# to the final link. +filegroup( + name = "headers", + # exclude the make-generated file that may be lying around in a dirty + # worktree; buck generates its own (:webgl-constants-sorted) + srcs = glob(["*.h"], exclude = ["ejs-webgl-constants-sorted.h"]), + visibility = ["PUBLIC"], +) + +shared_sources = [ + "ejs-arguments.c", + "ejs-array.c", + "ejs-boolean.c", + "ejs-closureenv.c", + "ejs-console.c", + "ejs-date.c", + "ejs-error.c", + "ejs-exception.c", + "ejs-function.c", + "ejs-gc.c", + "ejs-gc-debug.c", + "ejs-gc-heap.c", + "ejs-gc-major.c", + "ejs-gc-mark.c", + "ejs-gc-minor.c", + "ejs-generator.c", + "ejs-init.c", + "ejs-json.c", + "ejs-map.c", + "ejs-math.c", + "ejs-module.c", + "ejs-number.c", + "ejs-object.c", + "ejs-ops.c", + "ejs-process.c", + "ejs-promise.c", + "ejs-proxy.c", + "ejs-recording.c", + "ejs-reflect.c", + "ejs-regexp.c", + "ejs-require.c", + "ejs-set.c", + "ejs-shapes.c", + "ejs-stream.c", + "ejs-string.c", + "ejs-symbol.c", + "ejs-timers.c", + "ejs-typedarrays.c", + "ejs-types.c", + "ejs-uri.c", + "ejs-weakmap.c", + "ejs-weakset.c", + "main.c", +] + +darwin_sources = [ + "ejs-jsobjc.m", + "ejs-log.m", + "ejs-objc.m", + "ejs-webgl.m", + "ejs-xhr.m", + "ejs-runloop-darwin.m", +] + +cxx_library( + name = "echo", + srcs = shared_sources + [ + ":atoms", + "//external-deps:parson.c", + ] + select({ + # non-darwin platforms use the plain-C log impl (darwin's + # ejs-log.m lives in :echo-objc) + "DEFAULT": ["ejs-runloop-noop.c", "ejs-log.c"], + "config//os:linux": ["ejs-runloop-libuv.c", "ejs-log.c"], + # the darwin (objc) sources live in :echo-objc; the system cxx + # toolchain has no objc compiler + "config//os:macos": [], + }), + header_namespace = "", + exported_headers = glob(["*.h"], exclude = ["ejs-webgl-constants-sorted.h"]), + headers = { + "ejs-webgl-constants-sorted.h": ":webgl-constants-sorted", + }, + compiler_flags = EJS_COMPILER_FLAGS + select({ + "DEFAULT": [], + # several runtime headers use objc types on macos, so the C sources + # get compiled as objective-c, same as the -ObjC in runtime/Makefile + "config//os:macos": [ + "-x", + "objective-c", + "-fno-objc-arc", + ], + }), + preferred_linkage = "static", + deps = [ + "//external-deps:double-conversion-headers", + "//external-deps:parson-headers", + "//external-deps:pcre-headers", + ], + visibility = ["PUBLIC"], +) + +# C++ half of the runtime; kept separate so the -x objective-c above doesn't +# apply to it. Merged into libecho.a by //:srcdir-tree. +cxx_library( + name = "echo-dtoa", + srcs = ["ejs-dtoa.cpp"], + header_namespace = "", + compiler_flags = EJS_COMPILER_FLAGS, + preferred_linkage = "static", + deps = [ + ":echo", + "//external-deps:double-conversion-headers", + ], + visibility = ["PUBLIC"], +) + +# The prelude's system cxx toolchain can't compile .m files, so alias them +# to .c and force the language with -x objective-c. The resulting archive +# is merged into libecho.a by //:srcdir-tree (libtool -static). +[ + genrule( + name = m + ".c", + srcs = [m], + out = m + ".c", + cmd = "cp $SRCDIR/" + m + " $OUT", + ) + for m in darwin_sources +] + +cxx_library( + name = "echo-objc", + srcs = [":" + m + ".c" for m in darwin_sources], + header_namespace = "", + headers = { + "ejs-webgl-constants-sorted.h": ":webgl-constants-sorted", + }, + compiler_flags = EJS_COMPILER_FLAGS + [ + "-x", + "objective-c", + "-DOBJC=1", + "-fno-objc-arc", + ], + preferred_linkage = "static", + deps = [":echo"], + visibility = ["PUBLIC"], +) diff --git a/runtime/Makefile b/runtime/Makefile deleted file mode 100644 index bf0d2eff..00000000 --- a/runtime/Makefile +++ /dev/null @@ -1,265 +0,0 @@ -TOP=.. - -include $(TOP)/build/config.mk - -LIBRARY=libecho.a -C_SOURCES= \ - ejs-arguments.c \ - ejs-array.c \ - ejs-boolean.c \ - ejs-closureenv.c \ - ejs-console.c \ - ejs-date.c \ - ejs-error.c \ - ejs-exception.c \ - ejs-function.c \ - ejs-gc.c \ - ejs-generator.c \ - ejs-init.c \ - ejs-json.c \ - ejs-map.c \ - ejs-math.c \ - ejs-module.c \ - ejs-number.c \ - ejs-object.c \ - ejs-ops.c \ - ejs-process.c \ - ejs-promise.c \ - ejs-proxy.c \ - ejs-recording.c \ - ejs-reflect.c \ - ejs-regexp.c \ - ejs-require.c \ - ejs-set.c \ - ejs-stream.c \ - ejs-string.c \ - ejs-symbol.c \ - ejs-timers.c \ - ejs-typedarrays.c \ - ejs-types.c \ - ejs-uri.c \ - ejs-weakmap.c \ - ejs-weakset.c \ - parson.c - -CPP_SOURCES= \ - ejs-dtoa.cpp - -VPATH=.:../external-deps/parson - -ejs-atoms-gen.c: ejs-atoms.h gen-atoms.js - @echo [GEN] $@ && ./gen-atoms.js $< > .tmp-$@ && mv .tmp-$@ $@ - -ifeq ($(HOST_OS),linux) -ALL_LIBRARIES=$(LIBRARY) -ALL_TARGETS=$(ALL_LIBRARIES) - -ifeq ($(EJS_RUNLOOP_IMPL),noop) -RUNLOOP_DEF=-DNOOP_RUNLOOP=1 -RUNLOOP_C_SOURCE=ejs-runloop-noop.c -else -RUNLOOP_DEF=-DHAVE_LIBUV=1 -RUNLOOP_C_SOURCE=ejs-runloop-libuv.c -endif - -LINUX_OBJECTS=$(C_SOURCES:%.c=%.o.linux) $(CPP_SOURCES:%.cpp=%.o.linux) $(OBJC_SOURCES:%.m=%.o.linux) ejs-log.o.linux main.o.linux ejs-invoke-closure-catch.o.linux $(RUNLOOP_C_SOURCE:%.c=%.o.linux) - -ALL_OBJECTS=$(LINUX_OBJECTS) - -CFLAGS += -I/usr/include/libunwind -I../external-deps/pcre-linux -I../external-deps/double-conversion - -ejs-init.o.linux: ejs-atoms-gen.c - -$(LIBRARY): $(LINUX_OBJECTS) - @echo [ar linux] $@ && /usr/bin/ar rc $@ $(LINUX_OBJECTS) - -OBJC_FLAGS= -ObjC -DOBJC=1 -fobjc-abi-version=2 -fobjc-legacy-dispatch - -%.o.linux: %.c - @mkdir -p .deps - @$(CC) -MM $(LINUX_CFLAGS) $< | sed -e s/`echo $@ | sed -e s,.linux,,`/$@/ > .deps/$@-deps - @echo [$(CC) linux] $< && $(CC) $(LINUX_CFLAGS) -c -o $@ $< - -%.o.linux: %.cpp - @mkdir -p .deps - @$(CXX) -MM $(LINUX_CFLAGS) $< | sed -e s/`echo $@ | sed -e s,.linux,,`/$@/ > .deps/$@-deps - @echo [$(CXX) linux] $< && $(CXX) -std=c++11 $(LINUX_CFLAGS) -c -o $@ $< - -%.o.linux: %.m - @mkdir -p .deps - @$(CC) -MM $(LINUX_CFLAGS) $< | sed -e s/`echo $@ | sed -e s,.linux,,`/$@/ > .deps/$@-deps - @echo [$(CC) linux] $< && $(CC) $(LINUX_CFLAGS) $(OBJC_FLAGS) -c -o $@ $< - -%.o.linux: %.ll - @echo [llc linux] $< && llc$(LLVM_SUFFIX) --relocation-model=pic -filetype=obj -o=$@ -O2 $< - --include $(patsubst %.o.linux,.deps/%.o.linux-deps,$(LINUX_OBJECTS)) -endif - -ifeq ($(HOST_OS),darwin) - -OBJC_SOURCES= \ - ejs-jsobjc.m \ - ejs-log.m \ - ejs-objc.m \ - ejs-webgl.m \ - ejs-xhr.m \ - ejs-runloop-darwin.m - -OSX_OBJECTS=$(C_SOURCES:%.c=%.o.osx) $(CPP_SOURCES:%.cpp=%.o.osx) $(OBJC_SOURCES:%.m=%.o.osx) ejs-invoke-closure-catch.o.osx main.o.osx -SIM_OBJECTS=$(C_SOURCES:%.c=%.o.sim) $(CPP_SOURCES:%.cpp=%.o.sim) $(OBJC_SOURCES:%.m=%.o.sim) ejs-invoke-closure-catch.o.sim main.o.sim -DEV_OBJECTS=$(C_SOURCES:%.c=%.o.armv7) $(CPP_SOURCES:%.cpp=%.o.armv7) $(OBJC_SOURCES:%.m=%.o.armv7) ejs-invoke-closure-catch-sret.o.armv7 main.o.armv7 -DEVS_OBJECTS=$(C_SOURCES:%.c=%.o.armv7s) $(CPP_SOURCES:%.cpp=%.o.armv7s) $(OBJC_SOURCES:%.m=%.o.armv7s) ejs-invoke-closure-catch.o.armv7s main.o.armv7s - -analyze_plists_c = $(C_SOURCES:%.c=%.plist) main.plist -analyze_plists_objc = $(OBJC_SOURCES:%.m=%.plist) - -OSX_LIBRARY=$(LIBRARY) -SIM_LIBRARY=$(LIBRARY).sim -DEV_LIBRARY=$(LIBRARY).armv7 -DEVS_LIBRARY=$(LIBRARY).armv7s - -ifneq ($(CIRCLE_BUILD_NUM),) -# on circleci we only build the osx library -ALL_LIBRARIES=$(OSX_LIBRARY) -ALL_TARGETS=$(ALL_LIBRARIES) -else -# on local builds we build all the libraries (XXX need to figure out how to accurately target those platforms first) -ALL_LIBRARIES=$(OSX_LIBRARY) -ALL_TARGETS=$(ALL_LIBRARIES) $(analyze_plists_c) $(analyze_plists_objc) -endif - -ALL_OBJECTS=$(SIM_OBJECTS) $(DEV_OBJECTS) $(DEVS_OBJECTS) $(OSX_OBJECTS) - -CFLAGS += -I../external-deps/pcre-osx -I../external-deps/double-conversion -IOSSIM_CFLAGS += -I../external-deps/pcre-iossim -I../external-deps/double-conversion -IOSDEV_CFLAGS += -I../external-deps/pcre-iosdev -I../external-deps/double-conversion - - -$(OSX_LIBRARY): $(OSX_OBJECTS) - @echo [ar osx] $@ && /usr/bin/ar rc $@ $(OSX_OBJECTS) - -$(SIM_LIBRARY): $(SIM_OBJECTS) - @echo [ar sim] $@ && /usr/bin/ar rc $@ $(SIM_OBJECTS) - -$(DEV_LIBRARY): $(DEV_OBJECTS) - @echo [ar armv7] $@ && /usr/bin/ar rc $@ $(DEV_OBJECTS) - -$(DEVS_LIBRARY): $(DEVS_OBJECTS) - @echo [ar armv7s] $@ && /usr/bin/ar rc $@ $(DEVS_OBJECTS) - -ejs-init.o.osx ejs-init.o.sim ejs-init.o.armv7 ejs-init.o.armv7s: ejs-atoms-gen.c - -ejs-webgl-constants-sorted.h: ejs-webgl-constants.h - @echo [GEN] $@ && (grep WEBGL_CONSTANT $< | sort > $@) - -ejs-webgl.o.osx ejs-webgl.o.sim ejs-webgl.o.armv7 ejs-webgl.o.armv7s: ejs-webgl-constants-sorted.h - - -OBJC_FLAGS= -ObjC -DOBJC=1 -fobjc-abi-version=2 -fobjc-legacy-dispatch -fno-objc-arc - -%.o.osx: %.c - @mkdir -p .deps - @$(CC) -MM $(OSX_CFLAGS) $< | sed -e s/`echo $@ | sed -e s,.osx,,`/$@/ > .deps/$@-deps - @echo [$(CC) osx] $< && $(CC) -ObjC $(OSX_CFLAGS) -c -o $@ $< - -%.o.osx: %.cpp - @mkdir -p .deps - @$(CXX) -MM $(OSX_CFLAGS) $< | sed -e s/`echo $@ | sed -e s,.osx,,`/$@/ > .deps/$@-deps - @echo [$(CXX) osx] $< && $(CXX) -std=c++11 $(OSX_CFLAGS) -c -o $@ $< - -%.o.osx: %.m - @mkdir -p .deps - @$(CC) -MM $(OSX_CFLAGS) $< | sed -e s/`echo $@ | sed -e s,.osx,,`/$@/ > .deps/$@-deps - @echo [$(CC) osx] $< && $(CC) $(OSX_CFLAGS) $(OBJC_FLAGS) -c -o $@ $< - -%.o.osx: %.ll - @echo [llc osx] $< && llc$(LLVM_SUFFIX) -mtriple=$(OSX_MTRIPLE) -filetype=obj -o=$@ -O2 $< - -%.o.sim: %.c - @mkdir -p .deps - @$(CC) -MM $(IOSSIM_CFLAGS) $< | sed -e s/`echo $@ | sed -e s,.sim,,`/$@/ > .deps/$@-deps - @echo [$(CC) sim] $< && PATH=$(IOSSIM_BIN):$$PATH $(CC) -ObjC $(IOSSIM_CFLAGS) -c -o $@ $< - -%.o.sim: %.cpp - @mkdir -p .deps - @$(CXX) -MM $(IOSSIM_CFLAGS) $< | sed -e s/`echo $@ | sed -e s,.sim,,`/$@/ > .deps/$@-deps - @echo [$(CXX) sim] $< && PATH=$(IOSSIM_BIN):$$PATH $(CXX) -std=c++11 $(IOSSIM_CFLAGS) -c -o $@ $< - -%.o.sim: %.m - @mkdir -p .deps - @$(CC) -MM -ObjC $(IOSSIM_CFLAGS) $< | sed -e s/`echo $@ | sed -e s,.sim,,`/$@/ > .deps/$@-deps - @echo [$(CC) sim] $< && PATH=$(IOSSIM_BIN):$$PATH $(CC) $(IOSSIM_CFLAGS) $(OBJC_FLAGS) -c -o $@ $< - -%.o.sim: %.ll - @echo [llc sim] $< && llc$(LLVM_SUFFIX) -march=x86 -mtriple=$(IOSSIM_MTRIPLE) -filetype=obj -o=$@ -O2 $< - -%.o.armv7: %.c - @mkdir -p .deps - @$(CC) -MM -ObjC $(IOSDEV_CFLAGS) $< | sed -e s/`echo $@ | sed -e s,.armv7,,`/$@/ > .deps/$@-deps - @echo [$(CC) armv7] $< && PATH=$(IOSDEV_BIN):$$PATH $(CC) -ObjC $(IOSDEV_CFLAGS) -c -o $@ $< - -%.o.armv7: %.cpp - @mkdir -p .deps - @$(CXX) -MM $(IOSDEV_CFLAGS) $< | sed -e s/`echo $@ | sed -e s,.armv7,,`/$@/ > .deps/$@-deps - @echo [$(CXX) armv7] $< && PATH=$(IOSDEV_BIN):$$PATH $(CXX) -std=c++11 $(IOSDEV_CFLAGS) -c -o $@ $< - -%.o.armv7: %.m - @mkdir -p .deps - @$(CC) -MM -ObjC $(IOSDEV_CFLAGS) $< | sed -e s/`echo $@ | sed -e s,.armv7,,`/$@/ > .deps/$@-deps - @echo [$(CC) armv7] $< && PATH=$(IOSDEV_BIN):$$PATH $(CC) $(IOSDEV_CFLAGS) $(OBJC_FLAGS) -c -o $@ $< - -%.o.armv7: %.ll - @echo [llc armv7] $< && llc$(LLVM_SUFFIX) -march=arm -mtriple=$(IOSDEV_MTRIPLE) -filetype=obj -o=$@ -O2 $< - -%.o.armv7s: %.c - @mkdir -p .deps - @$(CC) -MM -ObjC $(IOSDEVS_CFLAGS) $< | sed -e s/`echo $@ | sed -e s,.armv7s,,`/$@/ > .deps/$@-deps - @echo [$(CC) armv7s] $< && PATH=$(IOSDEVS_BIN):$$PATH $(CC) -ObjC $(IOSDEVS_CFLAGS) -c -o $@ $< - -%.o.armv7s: %.cpp - @mkdir -p .deps - @$(CXX) -MM $(IOSDEVS_CFLAGS) $< | sed -e s/`echo $@ | sed -e s,.armv7s,,`/$@/ > .deps/$@-deps - @echo [$(CXX) armv7s] $< && PATH=$(IOSDEVS_BIN):$$PATH $(CXX) -std=c++11 $(IOSDEVS_CFLAGS) -c -o $@ $< - -%.o.armv7s: %.m - @mkdir -p .deps - @$(CC) -MM -ObjC $(IOSDEVS_CFLAGS) $< | sed -e s/`echo $@ | sed -e s,.armv7s,,`/$@/ > .deps/$@-deps - @echo [$(CC) armv7] $< && PATH=$(IOSDEVS_BIN):$$PATH $(CC) $(IOSDEVS_CFLAGS) $(OBJC_FLAGS) -c -o $@ $< - -%.o.armv7s: %.ll - @echo [llc armv7s] $< && llc$(LLVM_SUFFIX) -march=arm -mtriple=$(IOSDEVS_MTRIPLE) -filetype=obj -o=$@ -O2 $< - -$(analyze_plists_c): %.plist: %.c - @echo [$(CC) analyze] $< && PATH=$(IOSDEVS_BIN):$$PATH $(CC) $(OSX_CFLAGS) --analyze $< -o $@ - -$(analyze_plists_objc): %.plist: %.m - @echo [$(CC) analyze] $< && PATH=$(IOSDEVS_BIN):$$PATH $(CC) $(OSX_CFLAGS) --analyze $< -o $@ - - -class-test: $(C_SOURCES:%.c=%.o.osx) $(OBJC_SOURCES:%.m=%.o.osx) class-test.o.osx - @echo [$(CC) osx] $< - $(CC) -ObjC $(OSX_CFLAGS) -o $@ $(C_SOURCES:%.c=%.o.osx) $(OBJC_SOURCES:%.m=%.o.osx) class-test.o.osx ../external-deps/pcre-osx/.libs/libpcre16.a -framework Foundation -framework AppKit -lstdc++ - - --include $(patsubst %.o.osx,.deps/%.o.osx-deps,$(OSX_OBJECTS)) --include $(patsubst %.o.sim,.deps/%.o.sim-deps,$(SIM_OBJECTS)) --include $(patsubst %.o.armv7,.deps/%.o.armv7-deps,$(DEV_OBJECTS)) --include $(patsubst %.o.armv7s,.deps/%.o.armv7s-deps,$(DEVS_OBJECTS)) -endif - -all-local:: $(ALL_TARGETS) - -#XXX(toshok) same as node-compat's Makefile - we need to install all targets to their respective archlibdirs -install-local:: - @$(MKDIR) $(includedir)/runtime - @$(MKDIR) $(archlibdir) - $(INSTALL) -c $(ALL_LIBRARIES) $(archlibdir) - @for i in *.h; do \ - $(INSTALL) -c $$i $(includedir)/runtime; \ - done - -clean-local:: - rm -f test $(ALL_OBJECTS) $(ALL_LIBRARIES) ejs-atoms-gen.c $(analyze_plists_c) $(analyze_plists_objc) - -include $(TOP)/build/build.mk diff --git a/runtime/ejs-arguments.c b/runtime/ejs-arguments.c index c214c5f5..76f2fedb 100644 --- a/runtime/ejs-arguments.c +++ b/runtime/ejs-arguments.c @@ -63,6 +63,17 @@ _ejs_arguments_new (int numElements, ejsval* args) return OBJECT_TO_EJSVAL(arguments); } +// the compiler's arg_len op: the length the arguments object (or the +// rest array starting at `index`) would report for a call that arrived +// with `argc` arguments, without materializing either object. Minted +// by the EIR args sinking (docs/sinking-plan.md, sinking-P3) when the +// object's only uses are `.length` reads. +ejsval +_ejs_arg_length (uint32_t argc, uint32_t index) +{ + return NUMBER_TO_EJSVAL(argc > index ? (double)(argc - index) : 0); +} + void _ejs_arguments_init(ejsval global) { @@ -75,6 +86,10 @@ _ejs_arguments_specop_get (ejsval obj, ejsval propertyName, ejsval receiver) { EJSArguments* arguments = EJSVAL_TO_ARGUMENTS(obj); + // symbol keys (@@iterator in particular — spreading `arguments` + // looks it up) can never be indices, and ToNumber on a symbol + // throws; they live in the ordinary property map + if (!EJSVAL_IS_SYMBOL(propertyName)) { // check if propertyName is an integer, or a string that we can convert to an int EJSBool is_index = EJS_FALSE; ejsval idx_val = ToNumber(propertyName); @@ -88,12 +103,12 @@ _ejs_arguments_specop_get (ejsval obj, ejsval propertyName, ejsval receiver) } if (is_index) { - if (idx < 0 || idx > arguments->argc) { - printf ("getprop(%d) on an arguments, returning undefined\n", idx); + if (idx < 0 || idx >= arguments->argc) { return _ejs_undefined; } return arguments->args[idx]; } + } // we also handle the length getter here if (EJSVAL_IS_STRING(propertyName) && !ucs2_strcmp (_ejs_ucs2_length, EJSVAL_TO_FLAT_STRING(propertyName))) { @@ -108,6 +123,8 @@ static EJSBool _ejs_arguments_specop_has_property (ejsval obj, ejsval propertyName) { EJSArguments* arguments = (EJSArguments*)EJSVAL_TO_OBJECT(obj); + if (EJSVAL_IS_SYMBOL(propertyName)) + return _ejs_Object_specops.HasProperty (obj, propertyName); // check if propertyName is an integer, or a string that we can convert to an int ejsval idx_val = ToNumber(propertyName); int idx; @@ -134,7 +151,7 @@ _ejs_arguments_specop_scan (EJSObject* obj, EJSValueFunc scan_func) { EJSArguments* args = (EJSArguments*)obj; for (int i = 0; i < args->argc; i ++) - scan_func (args->args[i]); + scan_func (&(args->args[i])); _ejs_Object_specops.Scan (obj, scan_func); } diff --git a/runtime/ejs-arguments.h b/runtime/ejs-arguments.h index 47040396..e5fea30c 100644 --- a/runtime/ejs-arguments.h +++ b/runtime/ejs-arguments.h @@ -31,6 +31,7 @@ extern EJSSpecOps _ejs_Arguments_specops; void _ejs_arguments_init(ejsval global); ejsval _ejs_arguments_new (int numElements, ejsval* args); +ejsval _ejs_arg_length (uint32_t argc, uint32_t index); EJS_END_DECLS diff --git a/runtime/ejs-array.c b/runtime/ejs-array.c index 3f93b824..e180b6bf 100644 --- a/runtime/ejs-array.c +++ b/runtime/ejs-array.c @@ -215,10 +215,20 @@ _ejs_array_new (int64_t numElements, EJSBool fill) rv->dense.array_alloc = numElements + 5; rv->dense.elements = (ejsval*)malloc(rv->dense.array_alloc * sizeof (ejsval)); - if (fill) { - for (int i = 0; i < numElements; i ++) - rv->dense.elements[i] = MAGIC_TO_EJSVAL_IMPL(EJS_ARRAY_HOLE); - } + // ALWAYS initialize [0, numElements): array_length is published + // below, so the scan specop walks these slots — and a GC can run + // before the caller stores a single element. Recycled malloc + // memory holds stale ejsvals (dead young pointers), and the + // mover evacuates whatever the scan reads: on glibc this was a + // deterministic poison-evacuation crash (fill=false callers + // like splice were a scan-of-garbage window on every platform, + // macos just kept surviving it by allocator-content luck). + // `fill` now only distinguishes "caller wants holes" from + // "caller overwrites immediately" — both get holes, the flag + // stays for the call sites' documentation value. + (void)fill; + for (int i = 0; i < numElements; i ++) + rv->dense.elements[i] = MAGIC_TO_EJSVAL_IMPL(EJS_ARRAY_HOLE); } rv->array_length = numElements; @@ -285,6 +295,7 @@ _ejs_array_push_dense(ejsval array, int argc, ejsval *args) EJSArray *arr = (EJSArray*)EJSVAL_TO_OBJECT(array); maybe_realloc_dense (arr, arr->array_length + argc); memmove (&EJSDENSEARRAY_ELEMENTS(arr)[EJSARRAY_LEN(arr)], args, argc * sizeof(ejsval)); + for (uint32_t _wb = 0; _wb < (uint32_t)argc; _wb++) _ejs_gc_remember(arr, args[_wb]); EJSARRAY_LEN(arr) += argc; return EJSARRAY_LEN(arr); } @@ -382,6 +393,7 @@ static EJS_NATIVE_FUNC(_ejs_Array_impl) { arr->dense.elements = (ejsval*)malloc(arr->dense.array_alloc * sizeof (ejsval)); memmove (arr->dense.elements, args, argc * sizeof(ejsval)); + for (uint32_t _wb = 0; _wb < (uint32_t)argc; _wb++) _ejs_gc_remember(arr, args[_wb]); } @@ -1474,7 +1486,9 @@ static EJS_NATIVE_FUNC(_ejs_Array_prototype_reduceRight) { k--; } // c. If kPresent is false, throw a TypeError exception. - _ejs_throw_nativeerror_utf8 (EJS_TYPE_ERROR, "Reduce right of empty array with no initial value"); + if (!kPresent) { + _ejs_throw_nativeerror_utf8 (EJS_TYPE_ERROR, "Reduce right of empty array with no initial value"); + } } // 10. Repeat, while k ≥ 0 while (k >= 0) { @@ -2049,6 +2063,7 @@ static EJS_NATIVE_FUNC(_ejs_Array_prototype_unshift) { int len = EJS_ARRAY_LEN(*_this); memmove (EJS_DENSE_ARRAY_ELEMENTS(*_this) + argc, EJS_DENSE_ARRAY_ELEMENTS(*_this), sizeof(ejsval) * len); memmove (EJS_DENSE_ARRAY_ELEMENTS(*_this), args, sizeof(ejsval) * argc); + for (uint32_t _wb = 0; _wb < (uint32_t)argc; _wb++) _ejs_gc_remember(EJSVAL_TO_OBJECT(*_this), args[_wb]); EJS_ARRAY_LEN(*_this) += argc; return NUMBER_TO_EJSVAL(len + argc); } @@ -2153,17 +2168,22 @@ static ejsval _ejs_array_slice_dense (ejsval env, ejsval _this, uint32_t argc, ejsval* args) { int len = EJS_ARRAY_LEN(_this); - int begin = argc > 0 ? (int)EJSVAL_TO_NUMBER(args[0]) : 0; - int end = argc > 1 ? (int)EJSVAL_TO_NUMBER(args[1]) : len; + int begin = argc > 0 && !EJSVAL_IS_UNDEFINED(args[0]) ? (int)EJSVAL_TO_NUMBER(args[0]) : 0; + int end = argc > 1 && !EJSVAL_IS_UNDEFINED(args[1]) ? (int)EJSVAL_TO_NUMBER(args[1]) : len; - begin = MIN(begin, len); - end = MIN(end, len); + // negative indices count from the end (ES6 22.1.3.22 steps 5/7) + if (begin < 0) begin = MAX(len + begin, 0); + else begin = MIN(begin, len); + if (end < 0) end = MAX(len + end, 0); + else end = MIN(end, len); - ejsval rv = ArraySpeciesCreate(_this, end-begin); + int count = MAX(end - begin, 0); + + ejsval rv = ArraySpeciesCreate(_this, count); memmove (&EJS_DENSE_ARRAY_ELEMENTS(rv)[0], &EJS_DENSE_ARRAY_ELEMENTS(_this)[begin], - (end-begin) * sizeof(ejsval)); + count * sizeof(ejsval)); return rv; } @@ -2707,6 +2727,105 @@ _ejs_array_init(ejsval global) #undef PROTO_ITER_METHOD } +// --- sparse (arraylet) element storage -------------------------------------- +// +// a sparse array's elements live in fixed-size, chunk-aligned arraylets +// (start_idx is a multiple of EJS_ARRAYLET_SIZE, alloc == length == +// EJS_ARRAYLET_SIZE, every slot initialized — holes are the same magic +// as dense holes). The arraylet list is kept sorted by start_idx; +// aligned chunks can never overlap. + +#define EJS_ARRAYLET_SIZE 512 + +// address of idx's slot, or NULL if its chunk doesn't exist (and +// create is false). Newly created chunks are all holes. +static ejsval* +sparse_element_addr (EJSArray* arr, int64_t idx, EJSBool create) +{ + int64_t chunk_start = idx & ~((int64_t)EJS_ARRAYLET_SIZE - 1); + + int lo = 0, hi = (int)arr->sparse.arraylet_num; + while (lo < hi) { + int mid = (lo + hi) / 2; + if (arr->sparse.arraylets[mid].start_idx < chunk_start) + lo = mid + 1; + else + hi = mid; + } + if (lo < arr->sparse.arraylet_num && arr->sparse.arraylets[lo].start_idx == chunk_start) + return &arr->sparse.arraylets[lo].elements[idx - chunk_start]; + + if (!create) + return NULL; + + if (arr->sparse.arraylet_num == arr->sparse.arraylet_alloc) { + arr->sparse.arraylet_alloc = arr->sparse.arraylet_alloc ? arr->sparse.arraylet_alloc * 2 : 5; + arr->sparse.arraylets = (Arraylet*)realloc (arr->sparse.arraylets, arr->sparse.arraylet_alloc * sizeof(Arraylet)); + } + memmove (&arr->sparse.arraylets[lo + 1], &arr->sparse.arraylets[lo], + (arr->sparse.arraylet_num - lo) * sizeof(Arraylet)); + arr->sparse.arraylet_num ++; + + Arraylet* al = &arr->sparse.arraylets[lo]; + al->start_idx = chunk_start; + al->length = EJS_ARRAYLET_SIZE; + al->alloc = EJS_ARRAYLET_SIZE; + al->elements = (ejsval*)malloc (EJS_ARRAYLET_SIZE * sizeof(ejsval)); + for (int i = 0; i < EJS_ARRAYLET_SIZE; i ++) + al->elements[i] = MAGIC_TO_EJSVAL_IMPL(EJS_ARRAY_HOLE); + + return &al->elements[idx - chunk_start]; +} + +// drop element storage at and above new_len (a length shrink) +static void +sparse_truncate (EJSArray* arr, int64_t new_len) +{ + while (arr->sparse.arraylet_num > 0) { + Arraylet* al = &arr->sparse.arraylets[arr->sparse.arraylet_num - 1]; + if (al->start_idx >= new_len) { + free (al->elements); + arr->sparse.arraylet_num --; + continue; + } + // sorted: only the last surviving chunk can straddle new_len + for (int64_t i = new_len - al->start_idx; i < al->length; i ++) + al->elements[i] = MAGIC_TO_EJSVAL_IMPL(EJS_ARRAY_HOLE); + break; + } +} + +// pushes the name ("0", "1", ...) of every present (non-hole) index +// property of array onto out (a dense array), in ascending order. +// Iterates storage, not length — a `new Array(1e9)` has no index +// properties and costs nothing here. +void +_ejs_array_push_own_index_names (ejsval array, ejsval out) +{ + if (EJSVAL_IS_SPARSE_ARRAY(array)) { + EJSArray* arr = (EJSArray*)EJSVAL_TO_OBJECT(array); + for (int i = 0; i < arr->sparse.arraylet_num; i ++) { + Arraylet* al = &arr->sparse.arraylets[i]; + for (int64_t j = 0; j < al->length; j ++) { + if (al->start_idx + j >= EJSARRAY_LEN(arr)) + break; + if (EJSVAL_IS_ARRAY_HOLE_MAGIC(al->elements[j])) + continue; + ejsval name = ToString(NUMBER_TO_EJSVAL(al->start_idx + j)); + _ejs_array_push_dense(out, 1, &name); + } + } + } + else { + for (int64_t i = 0; i < EJS_ARRAY_LEN(array); i ++) { + if (EJSVAL_IS_ARRAY_HOLE_MAGIC(EJS_DENSE_ARRAY_ELEMENTS(array)[i])) + continue; + ejsval name = ToString(NUMBER_TO_EJSVAL(i)); + _ejs_array_push_dense(out, 1, &name); + } + } +} + static ejsval _ejs_array_specop_get (ejsval obj, ejsval propertyName, ejsval receiver) { @@ -2731,7 +2850,16 @@ _ejs_array_specop_get (ejsval obj, ejsval propertyName, ejsval receiver) //printf ("getprop(%d) on an array, returning undefined\n", idx); return _ejs_undefined; } - ejsval rv = EJS_DENSE_ARRAY_ELEMENTS(obj)[idx]; + ejsval rv; + if (EJSVAL_IS_SPARSE_ARRAY(obj)) { + ejsval* slot = sparse_element_addr ((EJSArray*)EJSVAL_TO_OBJECT(obj), idx, EJS_FALSE); + if (!slot) + return _ejs_undefined; + rv = *slot; + } + else { + rv = EJS_DENSE_ARRAY_ELEMENTS(obj)[idx]; + } if (EJSVAL_IS_ARRAY_HOLE_MAGIC(rv)) return _ejs_undefined; return rv; @@ -2767,10 +2895,18 @@ _ejs_array_specop_get_own_property (ejsval obj, ejsval propertyName, ejsval *exc if (is_index) { if (idx >= 0 && idx < EJS_ARRAY_LEN(obj)) { + ejsval el; + if (EJSVAL_IS_SPARSE_ARRAY(obj)) { + ejsval* slot = sparse_element_addr ((EJSArray*)EJSVAL_TO_OBJECT(obj), idx, EJS_FALSE); + el = slot ? *slot : MAGIC_TO_EJSVAL_IMPL(EJS_ARRAY_HOLE); + } + else { + el = EJS_DENSE_ARRAY_ELEMENTS(obj)[idx]; + } // XXX we leak this. need to change get_own_property to use an out param instead of a return value EJSPropertyDesc* desc = (EJSPropertyDesc*)calloc(sizeof(EJSPropertyDesc), 1); _ejs_property_desc_set_writable (desc, EJS_TRUE); - _ejs_property_desc_set_value (desc, EJS_DENSE_ARRAY_ELEMENTS(obj)[idx]); + _ejs_property_desc_set_value (desc, el); return desc; } } @@ -2822,10 +2958,11 @@ _ejs_array_specop_set (ejsval obj, ejsval propertyName, ejsval val, ejsval recei } EJS_DENSE_ARRAY_ELEMENTS(obj)[idx] = val; + EJS_GC_REMEMBER(obj, val); } else { - // we're already sparse, just give up as none of this is implemented yet. - EJS_NOT_IMPLEMENTED(); + *sparse_element_addr ((EJSArray*)EJSVAL_TO_OBJECT(obj), idx, EJS_TRUE) = val; + EJS_GC_REMEMBER(obj, val); } EJS_ARRAY_LEN(obj) = MAX(EJS_ARRAY_LEN(obj), idx + 1); return EJS_TRUE; @@ -2846,9 +2983,9 @@ _ejs_array_specop_set (ejsval obj, ejsval propertyName, ejsval val, ejsval recei EJS_DENSE_ARRAY_ELEMENTS(obj)[i] = MAGIC_TO_EJSVAL_IMPL(EJS_ARRAY_HOLE); } } - else { - // we're already sparse, just give up as none of this is implemented yet. - EJS_NOT_IMPLEMENTED(); + else if (newLen < oldLen) { + // growth needs no storage (holes are implicit) + sparse_truncate ((EJSArray*)EJSVAL_TO_OBJECT(obj), newLen); } EJS_ARRAY_LEN(obj) = newLen; @@ -2875,7 +3012,16 @@ _ejs_array_specop_has_property (ejsval obj, ejsval propertyName) if (floor(n) == n) { idx = (int)n; if (idx >= 0 && idx < EJS_ARRAY_LEN(obj)) { - ejsval element = EJS_DENSE_ARRAY_ELEMENTS(obj)[idx]; + ejsval element; + if (EJSVAL_IS_SPARSE_ARRAY(obj)) { + ejsval* slot = sparse_element_addr ((EJSArray*)EJSVAL_TO_OBJECT(obj), idx, EJS_FALSE); + if (!slot) + return EJS_FALSE; + element = *slot; + } + else { + element = EJS_DENSE_ARRAY_ELEMENTS(obj)[idx]; + } if (EJSVAL_IS_ARRAY_HOLE_MAGIC(element)) return EJS_FALSE; return EJS_TRUE; @@ -2910,8 +3056,16 @@ _ejs_array_specop_delete (ejsval obj, ejsval propertyName, EJSBool flag) return _ejs_Object_specops.Delete (obj, propertyName, flag); // if it's outside the array bounds, do nothing - if (idx < EJS_ARRAY_LEN(obj)) - EJS_DENSE_ARRAY_ELEMENTS(obj)[idx] = MAGIC_TO_EJSVAL_IMPL(EJS_ARRAY_HOLE); + if (idx < EJS_ARRAY_LEN(obj)) { + if (EJSVAL_IS_SPARSE_ARRAY(obj)) { + ejsval* slot = sparse_element_addr ((EJSArray*)EJSVAL_TO_OBJECT(obj), idx, EJS_FALSE); + if (slot) + *slot = MAGIC_TO_EJSVAL_IMPL(EJS_ARRAY_HOLE); + } + else { + EJS_DENSE_ARRAY_ELEMENTS(obj)[idx] = MAGIC_TO_EJSVAL_IMPL(EJS_ARRAY_HOLE); + } + } return EJS_TRUE; } @@ -2957,10 +3111,11 @@ _ejs_array_specop_define_own_property (ejsval obj, ejsval propertyName, EJSPrope } EJS_DENSE_ARRAY_ELEMENTS(obj)[idx] = propertyDescriptor->value; + EJS_GC_REMEMBER(obj, propertyDescriptor->value); } else { - // we're already sparse, just give up as none of this is implemented yet. - EJS_NOT_IMPLEMENTED(); + *sparse_element_addr ((EJSArray*)EJSVAL_TO_OBJECT(obj), idx, EJS_TRUE) = propertyDescriptor->value; + EJS_GC_REMEMBER(obj, propertyDescriptor->value); } EJS_ARRAY_LEN(obj) = MAX(EJS_ARRAY_LEN(obj), idx + 1); return EJS_TRUE; @@ -2981,9 +3136,9 @@ _ejs_array_specop_define_own_property (ejsval obj, ejsval propertyName, EJSPrope EJS_DENSE_ARRAY_ELEMENTS(obj)[i] = MAGIC_TO_EJSVAL_IMPL(EJS_ARRAY_HOLE); } } - else { - // we're already sparse, just give up as none of this is implemented yet. - EJS_NOT_IMPLEMENTED(); + else if (newLen < oldLen) { + // growth needs no storage (holes are implicit) + sparse_truncate ((EJSArray*)EJSVAL_TO_OBJECT(obj), newLen); } EJS_ARRAY_LEN(obj) = newLen; @@ -3027,12 +3182,12 @@ _ejs_array_specop_scan (EJSObject* obj, EJSValueFunc scan_func) for (int i = 0; i < arr->sparse.arraylet_num; i ++) { Arraylet al = arr->sparse.arraylets[i]; for (int j = 0; j < al.length; j ++) - scan_func (al.elements[j]); + scan_func (&(al.elements[j])); } } else { for (int i = 0; i < EJSARRAY_LEN(obj); i ++) - scan_func (EJSDENSEARRAY_ELEMENTS(obj)[i]); + scan_func (&(EJSDENSEARRAY_ELEMENTS(obj)[i])); } _ejs_Object_specops.Scan (obj, scan_func); } @@ -3063,7 +3218,7 @@ static void _ejs_array_iterator_specop_scan (EJSObject* obj, EJSValueFunc scan_func) { EJSArrayIterator* iter = (EJSArrayIterator*)obj; - scan_func(iter->iterated); + scan_func(&(iter->iterated)); _ejs_Object_specops.Scan (obj, scan_func); } diff --git a/runtime/ejs-array.h b/runtime/ejs-array.h index ea2334a6..243cc882 100644 --- a/runtime/ejs-array.h +++ b/runtime/ejs-array.h @@ -97,6 +97,10 @@ void _ejs_array_init(ejsval global); uint32_t _ejs_array_push_dense (ejsval array, int argc, ejsval* args); ejsval _ejs_array_pop_dense (ejsval array); +// ascending "0","1",... names of the present index properties, pushed +// onto out (used by Object.getOwnPropertyNames) +void _ejs_array_push_own_index_names (ejsval array, ejsval out); + ejsval _ejs_array_join (ejsval array, ejsval sep); ejsval _ejs_array_from_iterables (int argc, ejsval* args); diff --git a/runtime/ejs-atoms.h b/runtime/ejs-atoms.h index 612c67e9..4c43d66e 100644 --- a/runtime/ejs-atoms.h +++ b/runtime/ejs-atoms.h @@ -353,6 +353,7 @@ EJS_ATOM(timeEnd) // gc functions EJS_ATOM(collect) +EJS_ATOM(heapSize) EJS_ATOM(dumpAllocationStats) EJS_ATOM(dumpLiveStrings) diff --git a/runtime/ejs-console.c b/runtime/ejs-console.c index 34aa9700..b9dffe79 100644 --- a/runtime/ejs-console.c +++ b/runtime/ejs-console.c @@ -42,6 +42,9 @@ console_toString(ejsval arg) { return EJSVAL_TO_SYMBOL(arg)->description; } else if (EJSVAL_IS_NUMBER(arg) || EJSVAL_IS_NUMBER_OBJECT(arg)) { + // node's inspect distinguishes -0 (ToString collapses it to "0") + if (EJSVAL_IS_NUMBER(arg) && EJSDOUBLE_IS_NEGZERO(EJSVAL_TO_NUMBER(arg))) + return _ejs_string_new_utf8("-0"); return _ejs_number_to_string(arg); } else if (EJSVAL_IS_ARRAY(arg)) { @@ -56,8 +59,14 @@ console_toString(ejsval arg) { ejsval content_strings = _ejs_array_new(EJS_ARRAY_LEN(arg), EJS_FALSE); // XXX the loop below assumes arg is a dense array EJS_ASSERT(EJSVAL_IS_DENSE_ARRAY(arg)); + ejsval quote = _ejs_string_new_utf8("'"); for (int i = 0; i < EJS_ARRAY_LEN(arg); i ++) { - EJS_DENSE_ARRAY_ELEMENTS(content_strings)[i] = console_toString(EJS_DENSE_ARRAY_ELEMENTS(arg)[i]); + ejsval el = EJS_DENSE_ARRAY_ELEMENTS(arg)[i]; + // node's inspect quotes strings nested inside arrays + ejsval el_str = EJSVAL_IS_STRING(el) + ? _ejs_string_concatv (quote, el, quote, _ejs_null) + : console_toString(el); + EJS_DENSE_ARRAY_ELEMENTS(content_strings)[i] = el_str; } ejsval contents = _ejs_array_join (content_strings, comma_space); diff --git a/runtime/ejs-dtoa.cpp b/runtime/ejs-dtoa.cpp index 67b64b09..edfbc74e 100644 --- a/runtime/ejs-dtoa.cpp +++ b/runtime/ejs-dtoa.cpp @@ -1,5 +1,5 @@ #include -#include "double-conversion/double-conversion.h" +#include "external-deps/double-conversion/double-conversion.h" using namespace double_conversion; diff --git a/runtime/ejs-error.c b/runtime/ejs-error.c index ad8d42ae..ae2e5d64 100644 --- a/runtime/ejs-error.c +++ b/runtime/ejs-error.c @@ -59,7 +59,8 @@ ejsval _ejs_URIError_prototype EJSVAL_ALIGNMENT; /* b. Let msgDesc be the PropertyDescriptor{[[Value]]: msg, [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: true}. */ \ /* c. Let status be DefinePropertyOrThrow(O, "message", msgDesc). */ \ /* d. Assert: status is not an abrupt completion. */ \ - _ejs_object_setprop (*_this, _ejs_atom_message, ToString(args[0])); \ + _ejs_object_define_value_property (*_this, _ejs_atom_message, msg, \ + EJS_PROP_NOT_ENUMERABLE | EJS_PROP_CONFIGURABLE | EJS_PROP_WRITABLE); \ } \ /* 5. Return O. */ \ return O; \ @@ -138,10 +139,13 @@ _ejs_error_init(ejsval global) ejsval toString = _ejs_function_new_native (_ejs_null, _ejs_atom_toString, _ejs_Error_prototype_toString); _ejs_gc_add_root (&toString); -#define EJS_ADD_NATIVE_ERROR_TYPE(err) EJS_MACRO_START \ +// proto_proto: Error.prototype chains to Object.prototype, the +// NativeError prototypes chain to Error.prototype (ES2015 19.5.6.3) — +// `e instanceof Error` must hold for every native error +#define EJS_ADD_NATIVE_ERROR_TYPE(err, proto_proto) EJS_MACRO_START \ _ejs_##err = _ejs_function_new_without_proto (_ejs_null, _ejs_atom_##err, _ejs_##err##_impl); \ _ejs_object_setprop (global, _ejs_atom_##err, _ejs_##err); \ - _ejs_##err##_prototype = _ejs_object_new(_ejs_null, &_ejs_Object_specops); \ + _ejs_##err##_prototype = _ejs_object_new(proto_proto, &_ejs_Object_specops); \ _ejs_object_setprop (_ejs_##err, _ejs_atom_prototype, _ejs_##err##_prototype); \ _ejs_object_define_value_property (_ejs_##err##_prototype, _ejs_atom_constructor, _ejs_##err,\ EJS_PROP_NOT_ENUMERABLE | EJS_PROP_CONFIGURABLE | EJS_PROP_WRITABLE); \ @@ -150,13 +154,13 @@ _ejs_error_init(ejsval global) _ejs_object_setprop (_ejs_##err##_prototype, _ejs_atom_toString, toString); \ EJS_MACRO_END - EJS_ADD_NATIVE_ERROR_TYPE(Error); - EJS_ADD_NATIVE_ERROR_TYPE(EvalError); - EJS_ADD_NATIVE_ERROR_TYPE(RangeError); - EJS_ADD_NATIVE_ERROR_TYPE(ReferenceError); - EJS_ADD_NATIVE_ERROR_TYPE(SyntaxError); - EJS_ADD_NATIVE_ERROR_TYPE(TypeError); - EJS_ADD_NATIVE_ERROR_TYPE(URIError); + EJS_ADD_NATIVE_ERROR_TYPE(Error, _ejs_Object_prototype); + EJS_ADD_NATIVE_ERROR_TYPE(EvalError, _ejs_Error_prototype); + EJS_ADD_NATIVE_ERROR_TYPE(RangeError, _ejs_Error_prototype); + EJS_ADD_NATIVE_ERROR_TYPE(ReferenceError, _ejs_Error_prototype); + EJS_ADD_NATIVE_ERROR_TYPE(SyntaxError, _ejs_Error_prototype); + EJS_ADD_NATIVE_ERROR_TYPE(TypeError, _ejs_Error_prototype); + EJS_ADD_NATIVE_ERROR_TYPE(URIError, _ejs_Error_prototype); _ejs_gc_remove_root (&toString); } diff --git a/runtime/ejs-exception.c b/runtime/ejs-exception.c index 363dc141..0eca1d84 100644 --- a/runtime/ejs-exception.c +++ b/runtime/ejs-exception.c @@ -15,7 +15,11 @@ #include -#define spew 1 +// off by default (same convention as ejs-gc-internal.h): the compiler +// resolves module imports by try/catch probing, so with spew on every +// compiled program logs a full throw/unwind/catch trace to stderr for +// each import miss (release-P1 follow-on) +#define spew 0 #if spew #define SPEW(x) x #else @@ -214,6 +218,10 @@ ejsval _ejs_begin_catch(void *exc_gen) #else struct ejs_exception *exc = (struct ejs_exception*)__cxa_begin_catch(exc_gen); #endif + // NOTE: &exc->val is rooted at throw and unrooted by the + // __cxa_throw destructor when the exception is released — the + // pairing is sound, and removing it here instead would race a + // same-address reallocation of the cxa buffer (found the hard way). return exc->val; } @@ -367,7 +375,7 @@ static intptr_t read_sleb(uintptr_t *pp) shift += 7; } while (byte & 0x80); if ((shift < 8*sizeof(intptr_t)) && (byte & 0x40)) { - result |= ((intptr_t)-1) << shift; + result |= ((uintptr_t)-1) << shift; } return result; } diff --git a/runtime/ejs-function.c b/runtime/ejs-function.c index 483ee975..a6848229 100644 --- a/runtime/ejs-function.c +++ b/runtime/ejs-function.c @@ -8,7 +8,9 @@ #include "ejs-value.h" #include "ejs-ops.h" +#include "ejs-gc.h" #include "ejs-object.h" +#include "ejs-shapes.h" #include "ejs-function.h" #include "ejs-proxy.h" #include "ejs-array.h" @@ -429,6 +431,34 @@ _ejs_invoke_closure (ejsval closure, ejsval* _this, uint32_t argc, ejsval* args, return OP(EJSVAL_TO_OBJECT(closure),Call) (closure, *_this, argc, args); } +// the .ll landing-pad wrappers (ejs-invoke-closure-catch.ll) +EJSBool _ejs_invoke_closure_catch_inner (ejsval* retval, ejsval closure, ejsval* _this, uint32_t argc, ejsval* args, ejsval newTarget); +EJSBool _ejs_invoke_func_catch_inner (ejsval* retval, ejsval(*func)(void*), void* data); + +// A C-side catch discards every emitted frame below it, but only +// emitted CATCH handlers re-link the gc-frame chain head — a C catcher +// must restore the head itself or the collector keeps walking the +// unwound (dead) frame records. +EJSBool +_ejs_invoke_closure_catch (ejsval* retval, ejsval closure, ejsval* _this, uint32_t argc, ejsval* args, ejsval newTarget) +{ + void* saved_gc_frame_head = _ejs_heap.gc_frame_head; + EJSBool ok = _ejs_invoke_closure_catch_inner (retval, closure, _this, argc, args, newTarget); + if (!ok) + _ejs_heap.gc_frame_head = saved_gc_frame_head; + return ok; +} + +EJSBool +_ejs_invoke_func_catch (ejsval* retval, ejsval(*func)(void*), void* data) +{ + void* saved_gc_frame_head = _ejs_heap.gc_frame_head; + EJSBool ok = _ejs_invoke_func_catch_inner (retval, func, data); + if (!ok) + _ejs_heap.gc_frame_head = saved_gc_frame_head; + return ok; +} + ejsval _ejs_construct_closure (ejsval _closure, ejsval* _this, uint32_t argc, ejsval* args, ejsval newTarget) { @@ -496,7 +526,7 @@ static void _ejs_function_specop_scan (EJSObject* obj, EJSValueFunc scan_func) { EJSFunction* f = (EJSFunction*)obj; - scan_func (f->env); + scan_func (&(f->env)); _ejs_Object_specops.Scan (obj, scan_func); } @@ -548,7 +578,12 @@ _ejs_function_specop_construct (ejsval F, ejsval newTarget, uint32_t argc, ejsva if (kind == CONSTRUCTOR_KIND_BASE) { // a. Let thisArgument be OrdinaryCreateFromConstructor(newTarget, "%ObjectPrototype%"). // b. ReturnIfAbrupt(thisArgument). - thisArgument = OrdinaryCreateFromConstructor(newTarget, _ejs_Object_prototype, &_ejs_Object_specops); + // gc-P5: the birth-capacity hint pre-sizes `this` so the + // constructor's slot fills stay in the object's own cell + // (single-cell allocation); semantics are unchanged from + // OrdinaryCreateFromConstructor with _ejs_Object_specops. + ejsval proto = GetPrototypeFromConstructor(newTarget, _ejs_Object_prototype); + thisArgument = _ejs_object_new_with_slot_hint (proto, F_->ctor_slot_hint); } // 6. Let calleeContext be PrepareForOrdinaryCall(F, newTarget). @@ -558,6 +593,20 @@ _ejs_function_specop_construct (ejsval F, ejsval newTarget, uint32_t argc, ejsva // 10. Let envRec be constructorEnv’s EnvironmentRecord. // 11. Let result be OrdinaryCallEvaluateBody(F, argumentsList). ejsval result = F_->func (F_->env, &thisArgument, argc, args, newTarget); + // birth-capacity feedback (gc-P5): remember how many fields the + // constructor installed so the NEXT base construct births `this` + // with embedded slot storage. One-shot 0 -> count; F_ is pinned by + // the conservative scan (it's C-stack-visible), so the pointer is + // stable across the body call. + if (kind == CONSTRUCTOR_KIND_BASE && F_->ctor_slot_hint == 0 + && EJSVAL_IS_OBJECT(thisArgument)) { + EJSObject* T_ = EJSVAL_TO_OBJECT(thisArgument); + if (T_->ops == &_ejs_Object_specops) { + uint32_t tshape = EJS_OBJECT_SHAPE(T_); + if (tshape != EJS_SHAPE_DICT) + F_->ctor_slot_hint = _ejs_shape_field_count (tshape); + } + } // 12. Remove calleeContext from the execution context stack and restore callerContext as the running execution context. // 13. If result.[[type]] is return, then // a. If Type(result.[[value]]) is Object, return NormalCompletion(result.[[value]]). diff --git a/runtime/ejs-function.h b/runtime/ejs-function.h index 8daec088..c4a4fbd8 100644 --- a/runtime/ejs-function.h +++ b/runtime/ejs-function.h @@ -32,6 +32,14 @@ typedef struct { EJSFunctionKind function_kind; EJSConstructorKind constructor_kind; + // birth-capacity hint (gc-P5): how many fields this function's + // constructor installed on its first `this` — subsequent base + // constructs allocate `this` with that many embedded slots so the + // result is a single cell. 0 = unknown/none. Occupies the + // struct's tail padding; compiled code never reads past `bound`, + // so lib/types.ts is unaffected. + uint32_t ctor_slot_hint; + } EJSFunction; diff --git a/runtime/ejs-gc-debug.c b/runtime/ejs-gc-debug.c new file mode 100644 index 00000000..42882b7c --- /dev/null +++ b/runtime/ejs-gc-debug.c @@ -0,0 +1,535 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=cpp: + */ + +// observability: EJS_GC_PROFILE instrumentation, EJS_GC_WATCH, +// EJS_GC_VERIFY barrier checks, EJS_GC_PARANOID heap validation, and +// the heap-stats dumps. + +#include "ejs-gc-internal.h" + +// ---- measurement instrumentation (EJS_GC_PROFILE=1) ----------- +// +// Two header bits from the gc-reserved range (57-63; see ejs-types.h — the +// shapes machinery masks its 24-bit index, so these are invisible to it): +// +// YOUNG: set at allocation, cleared on the first collection the object +// survives. "young" therefore means "allocated since the last +// collection" — exactly the population a generational nursery +// would manage, so per-cycle young-survival is THE +// number that sizes the nursery payoff. +// PINNED: set (once per cycle) when a CONSERVATIVE reference — C stack, +// spilled registers, generator stacks/contexts — hits the +// object. Under the mover these are the objects that cannot +// be evacuated this cycle; their count/bytes/sources size the +// payoff of precise JS frames and decide its ordering. +// +// The YOUNG bit is set unconditionally (an OR folded into the header +// store the allocator already does); everything else is gated on +// gc_profile so the measured path stays clean when profiling is off. +// (The YOUNG/PINNED #defines live near the top of the file — the mark +// helpers set PINNED for the compacting major.) + +EJSBool gc_profile; // EJS_GC_PROFILE (parsed in _ejs_gc_init) +struct timeval prof_start_tv; // process start, for the shutdown report + +// (the PROF_SRC_* enum lives in ejs-gc-internal.h; the scanners set +// prof_pin_source as they change source) +static const char* prof_src_names[PROF_SRC_COUNT] = { "cstack", "regs", "genstack" }; +int prof_pin_source = PROF_SRC_CSTACK; + +#define PROF_NBUCKETS 12 // ffs buckets 16B.. + [0] = LOS +static uint64_t prof_alloc_count[PROF_NBUCKETS]; +static uint64_t prof_alloc_bytes[PROF_NBUCKETS]; +static uint64_t prof_kind_count[4]; // primstr, primsym, object, closureenv +static uint64_t prof_alloc_total_count = 0; +static uint64_t prof_alloc_total_bytes = 0; +// the young population: allocations since the last collection +static uint64_t prof_young_count = 0; +static uint64_t prof_young_bytes = 0; +// per-cycle pin accounting (reset after each report) +static uint64_t prof_pin_count[PROF_SRC_COUNT]; +static uint64_t prof_pin_bytes[PROF_SRC_COUNT]; +static uint64_t prof_pin_young = 0, prof_pin_old = 0; +static uint64_t prof_pin_env_interior = 0, prof_pin_los = 0; +static uint64_t prof_collections = 0; +static uint64_t prof_total_pause_usec = 0; +const char* prof_gc_reason = "?"; + +void +profile_note_alloc(size_t size, int ffs_bucket, EJSScanType scan_type) +{ + int idx; + if (ffs_bucket > OBJECT_SIZE_HIGH_LIMIT_BITS + 1) + idx = 0; // LOS + else { + idx = ffs_bucket - OBJECT_SIZE_LOW_LIMIT_BITS; + if (idx < 1) idx = 1; + if (idx >= PROF_NBUCKETS) idx = PROF_NBUCKETS - 1; + } + prof_alloc_count[idx]++; + prof_alloc_bytes[idx] += size; + prof_alloc_total_count++; + prof_alloc_total_bytes += size; + switch (scan_type) { + case EJS_SCAN_TYPE_PRIMSTR: prof_kind_count[0]++; break; + case EJS_SCAN_TYPE_PRIMSYM: prof_kind_count[1]++; break; + case EJS_SCAN_TYPE_OBJECT: prof_kind_count[2]++; break; + case EJS_SCAN_TYPE_CLOSUREENV: prof_kind_count[3]++; break; + } + prof_young_count++; + prof_young_bytes += size; +} + +// a conservative reference hit an allocated cell: under the mover this +// object is pinned for the cycle. counted once per cycle per object +// (dedupe via the PINNED header bit), attributed to the scan source that +// found it first, split young/old, with env-interior-pointer and LOS +// sub-counts. runs BEFORE the white-check filter: a hit on an +// already-marked object still pins it. +void +profile_note_pin(PageInfo* page, uint32_t cell_idx, GCObjectPtr raw) +{ + GCObjectPtr base = page->page_start + (cell_idx * page->cell_size); + GCObjectHeader* h = (GCObjectHeader*)base; + if (*h & EJS_GC_HEADER_PINNED) + return; + *h |= EJS_GC_HEADER_PINNED; + prof_pin_count[prof_pin_source]++; + prof_pin_bytes[prof_pin_source] += page->cell_size; + if (*h & EJS_GC_HEADER_YOUNG) prof_pin_young++; else prof_pin_old++; + if (raw != base && (*h & EJS_SCAN_TYPE_CLOSUREENV)) prof_pin_env_interior++; + if (page->los_info) prof_pin_los++; +} + +// per-cycle results filled by profile_pre_sweep (which must run after +// marking and BEFORE the sweep frees the dead cells), printed with the +// pause by profile_report_cycle_end +static uint64_t prof_cycle_live_count, prof_cycle_live_bytes; +static uint64_t prof_cycle_ysurv_count, prof_cycle_ysurv_bytes; + +static void +profile_visit_live_cell(GCObjectHeader* h, size_t bytes) +{ + prof_cycle_live_count++; + prof_cycle_live_bytes += bytes; + if (*h & EJS_GC_HEADER_YOUNG) { + prof_cycle_ysurv_count++; + prof_cycle_ysurv_bytes += bytes; + *h &= ~EJS_GC_HEADER_YOUNG; // survived one collection: no longer young + } + // reset pins for the next cycle — but the census runs PRE-sweep and + // the compacting major reads pins POST-sweep (and clears them in its + // fixup walk); clearing here would un-pin every C-visible object + // right before evacuation decides what may move + if (!compact_enabled) + *h &= ~EJS_GC_HEADER_PINNED; +} + +void +profile_pre_sweep(void) +{ + prof_cycle_live_count = prof_cycle_live_bytes = 0; + prof_cycle_ysurv_count = prof_cycle_ysurv_bytes = 0; + for (int i = 0; i < HEAP_PAGELISTS_COUNT; i++) { + EJS_LIST_FOREACH (&heap_pages[i], PageInfo, page, { + GCObjectPtr p = page->page_start; + for (int c = 0; c < CELLS_IN_PAGE(page); c++, p += page->cell_size) { + BitmapCell cell = page->page_bitmap[c]; + if (cell_is_free(cell) || cell_is_white(cell)) continue; + profile_visit_live_cell((GCObjectHeader*)p, page->cell_size); + } + }); + } + for (LargeObjectInfo* lobj = los_list; lobj; lobj = lobj->next) { + BitmapCell cell = lobj->page_info.page_bitmap[0]; + if (cell_is_free(cell) || cell_is_white(cell)) continue; + profile_visit_live_cell((GCObjectHeader*)lobj->page_info.page_start, + lobj->page_info.cell_size); + } +} + +void +profile_report_cycle_end(uint64_t pause_usec) +{ + prof_collections++; + prof_total_pause_usec += pause_usec; + double surv_pct = prof_young_bytes + ? 100.0 * (double)prof_cycle_ysurv_bytes / (double)prof_young_bytes : 0.0; + _ejs_log ("EJS_GC_PROFILE: gc#%llu reason=%s pause=%.2fms " + "live=%llu objs/%.2fMB | young allocd=%llu/%.2fMB " + "survived=%llu/%.2fMB (%.1f%% of bytes) | pins: " + "cstack=%llu/%lluKB regs=%llu/%lluKB genstack=%llu/%lluKB " + "envint=%llu los=%llu young=%llu old=%llu\n", + (unsigned long long)prof_collections, prof_gc_reason, + pause_usec / 1000.0, + (unsigned long long)prof_cycle_live_count, + prof_cycle_live_bytes / (1024.0 * 1024.0), + (unsigned long long)prof_young_count, + prof_young_bytes / (1024.0 * 1024.0), + (unsigned long long)prof_cycle_ysurv_count, + prof_cycle_ysurv_bytes / (1024.0 * 1024.0), + surv_pct, + (unsigned long long)prof_pin_count[PROF_SRC_CSTACK], + (unsigned long long)(prof_pin_bytes[PROF_SRC_CSTACK] / 1024), + (unsigned long long)prof_pin_count[PROF_SRC_REGS], + (unsigned long long)(prof_pin_bytes[PROF_SRC_REGS] / 1024), + (unsigned long long)prof_pin_count[PROF_SRC_GENSTACK], + (unsigned long long)(prof_pin_bytes[PROF_SRC_GENSTACK] / 1024), + (unsigned long long)prof_pin_env_interior, + (unsigned long long)prof_pin_los, + (unsigned long long)prof_pin_young, + (unsigned long long)prof_pin_old); + prof_young_count = prof_young_bytes = 0; + memset (prof_pin_count, 0, sizeof (prof_pin_count)); + memset (prof_pin_bytes, 0, sizeof (prof_pin_bytes)); + prof_pin_young = prof_pin_old = 0; + prof_pin_env_interior = prof_pin_los = 0; +} + +void +profile_report_shutdown(void) +{ + static EJSBool reported = EJS_FALSE; // atexit + GC_ON_SHUTDOWN may both fire + if (reported) return; + reported = EJS_TRUE; + + struct timeval now; + gettimeofday (&now, NULL); + double wall = (now.tv_sec - prof_start_tv.tv_sec) + + (now.tv_usec - prof_start_tv.tv_usec) / 1e6; + _ejs_log ("EJS_GC_PROFILE: totals: allocs=%llu bytes=%.2fMB wall=%.2fs " + "(%.1fMB/s, %.0f allocs/s) collections=%llu total-pause=%.2fms\n", + (unsigned long long)prof_alloc_total_count, + prof_alloc_total_bytes / (1024.0 * 1024.0), wall, + prof_alloc_total_bytes / (1024.0 * 1024.0) / (wall > 0 ? wall : 1), + prof_alloc_total_count / (wall > 0 ? wall : 1), + (unsigned long long)prof_collections, + prof_total_pause_usec / 1000.0); + _ejs_log ("EJS_GC_PROFILE: kinds: primstr=%llu primsym=%llu object=%llu " + "closureenv=%llu\n", + (unsigned long long)prof_kind_count[0], + (unsigned long long)prof_kind_count[1], + (unsigned long long)prof_kind_count[2], + (unsigned long long)prof_kind_count[3]); + for (int i = 1; i < PROF_NBUCKETS; i++) { + if (!prof_alloc_count[i]) continue; + _ejs_log ("EJS_GC_PROFILE: size<=%4d: %llu allocs, %.2fMB requested\n", + 1 << (OBJECT_SIZE_LOW_LIMIT_BITS + i - 1), + (unsigned long long)prof_alloc_count[i], + prof_alloc_bytes[i] / (1024.0 * 1024.0)); + } + if (prof_alloc_count[0]) + _ejs_log ("EJS_GC_PROFILE: LOS: %llu allocs, %.2fMB requested\n", + (unsigned long long)prof_alloc_count[0], + prof_alloc_bytes[0] / (1024.0 * 1024.0)); +} + +// ======================= the nursery ============================ +// +// One dedicated arena; size-class pages inside it are bump-allocated +// (the seam's per-class bump/limit cursors ARE the allocation state — +// emitted code bumps them inline). Minor GC is mostly- +// copying: conservative hits pin young cells in place (established +// FIRST), then every precise slot — root list, module exports, +// remembered-set entries, and the transitive scan through the +// slot-based Scan protocol — evacuates its young referent into the old +// gen, installs a P1 forwarding record, and is rewritten. Young pages +// end the cycle reset (no survivors) or as survivor pages (pins only — +// pins merely delay promotion). The old gen stays mark-sweep. + +// EJS_GC_WATCH=: log every lifecycle event touching the cell +// containing that address, with a C backtrace (debugging aid for the +// deterministic single-cell corruption hunt) +#include +uintptr_t gc_watch_addr; +void +gc_watch_hit(const char* what, void* p) +{ + if (EJS_LIKELY(gc_watch_addr == 0)) return; + if ((uintptr_t)p > gc_watch_addr || gc_watch_addr - (uintptr_t)p >= 256) return; + _ejs_log ("EJS_GC_WATCH: %s cell=%p (minor#%llu, in_minor=%d)\n", + what, p, (unsigned long long)heap_priv.minors, (int)in_minor_gc); + void* frames[24]; + int n = backtrace (frames, 24); + backtrace_symbols_fd (frames, n, 2); +} + +// EJS_GC_PARANOID: reverse-lookup for the sweep's death detector — when +// a young cell dies, name everything that still references it (old gen, +// LOS, roots, modules, the C stack). A hit is a missed barrier/scan of +// that owner; zero hits means the pointer was in-flight in mutator +// state the conservative scan cannot see. +static GCObjectPtr referrer_target; +static const char* referrer_ctx; +static GCObjectPtr referrer_owner; +static int referrer_hits; +// the minor collection's entry frame pointer: the raw-stack sweep's +// floor (set per minor while EJS_GC_PARANOID is on) +void** paranoid_stack_floor; +static void +referrer_check_slot(ejsval* slot) +{ + ejsval v = *slot; + if (!EJSVAL_IS_TRACEABLE_IMPL(v)) return; + if ((GCObjectPtr)EJSVAL_TO_GCTHING_IMPL(v) == referrer_target) { + GCObjectHeader oh = referrer_owner ? *(GCObjectHeader*)referrer_owner : 0; + _ejs_log ("EJS_GC_PARANOID: dying young %p still referenced: ctx=%s owner=%p (hdr %llx) slot=%p\n", + referrer_target, referrer_ctx, (void*)referrer_owner, + (unsigned long long)oh, (void*)slot); + referrer_hits++; + } +} +static void +referrer_check_object(GCObjectPtr p) +{ + GCObjectHeader header = *(GCObjectHeader*)p; + referrer_owner = p; + if ((header & EJS_SCAN_TYPE_OBJECT) != 0) { + EJSObject* obj = (EJSObject*)p; + if (obj->ops != NULL) OP(obj,Scan)(obj, referrer_check_slot); + } else if ((header & EJS_SCAN_TYPE_CLOSUREENV) != 0) { + EJSClosureEnv* env = (EJSClosureEnv*)p; + for (uint32_t i = 0; i < env->length; i++) + referrer_check_slot(&env->slots[i]); + } else if ((header & EJS_SCAN_TYPE_PRIMSYM) != 0) { + referrer_check_slot(&((EJSPrimSymbol*)p)->description); + } +} +int +paranoid_report_referrers(GCObjectPtr p) +{ + referrer_target = p; + referrer_hits = 0; + referrer_ctx = "oldgen"; + old_gen_walk (referrer_check_object); + referrer_ctx = "roots"; + referrer_owner = NULL; + root_registry_foreach (referrer_check_slot); + referrer_ctx = "modules"; + for (int i = 0; i < _ejs_num_modules; i++) { + EJSObject* mod = (EJSObject*)_ejs_modules[i]; + referrer_owner = (GCObjectPtr)mod; + if (mod->ops) OP(mod,Scan)(mod, referrer_check_slot); + } + // raw C-stack sweep: any word whose payload lands inside the dying + // cell counts (boxed or raw, base or interior). Floor the sweep at + // the minor's entry frame: everything deeper is COLLECTOR frames — + // the sweep loop's own cell cursor, evacuation temporaries — written + // AFTER the conservative pin scan ran, so a hit there is the checker + // reading its own machinery, not a missed mutator reference. (The + // P6.3 file split's codegen shift surfaced exactly that self-hit.) + referrer_ctx = "stack"; + referrer_owner = NULL; + void* volatile probe; + void** stack_lo = paranoid_stack_floor ? paranoid_stack_floor : (void**)&probe; + for (void** w = stack_lo; w < (void**)stack_bottom; w++) { + uintptr_t masked = (uintptr_t)*w & 0x00007fffffffffffULL; + if ((char*)masked >= (char*)p && (char*)masked < (char*)p + 16) { + _ejs_log ("EJS_GC_PARANOID: dying young %p: raw stack word at %p = %p\n", + p, (void*)w, *w); + referrer_hits++; + } + } + return referrer_hits; +} + +// EJS_GC_VERIFY: after the remset has been processed, no live old slot +// may still reference an unforwarded, unpinned young object — such an +// edge is a missed write barrier. Report and abort. +ejsval* verify_bad_slot; +static void +verify_check_slot(ejsval* slot) +{ + ejsval v = *slot; + if (!EJSVAL_IS_TRACEABLE_IMPL(v)) return; + GCObjectPtr p = (GCObjectPtr)EJSVAL_TO_GCTHING_IMPL(v); + if (p == NULL || !_ejs_gc_is_young(p)) return; + uint32_t cell_idx; + PageInfo* page = find_page_and_cell(p, &cell_idx); + if (!page) return; + GCObjectPtr base = page->page_start + ((size_t)cell_idx * page->cell_size); + if (_ejs_gc_is_forwarded(base)) return; // will be rewritten by its recorder + if (cell_is_black(page->page_bitmap[cell_idx])) { minor_scan_saw_young = EJS_TRUE; return; } // pinned in place + verify_bad_slot = slot; +} +void +verify_check_object(GCObjectPtr p) +{ + GCObjectHeader header = *(GCObjectHeader*)p; + if ((header & EJS_SCAN_TYPE_OBJECT) != 0) { + EJSObject* obj = (EJSObject*)p; + if (obj->ops != NULL) + OP(obj,Scan)(obj, verify_check_slot); + if (verify_bad_slot) { + _ejs_log ("EJS_GC_VERIFY: missed write barrier: old object %p (class %s) slot %p holds unpromoted young ref (bits %llx)\n", + p, obj->ops ? obj->ops->class_name : "", + (void*)verify_bad_slot, + (unsigned long long)verify_bad_slot->asBits); + abort(); + } + } + else if ((header & EJS_SCAN_TYPE_CLOSUREENV) != 0) { + EJSClosureEnv* env = (EJSClosureEnv*)p; + for (uint32_t i = 0; i < env->length; i++) { + verify_check_slot(&env->slots[i]); + if (verify_bad_slot) { + _ejs_log ("EJS_GC_VERIFY: missed write barrier: old env %p (hdr %llx, len %u) slot %u holds unpromoted young ref (bits %llx, target hdr %llx)\n", + p, (unsigned long long)header, env->length, i, + (unsigned long long)verify_bad_slot->asBits, + (unsigned long long)*(GCObjectHeader*)EJSVAL_TO_GCTHING_IMPL(*verify_bad_slot)); + abort(); + } + } + } + else if ((header & EJS_SCAN_TYPE_PRIMSTR) != 0) { + EJSPrimString* ps = (EJSPrimString*)p; + EJSPrimString* kids[2] = { NULL, NULL }; + switch (EJS_PRIMSTR_GET_TYPE(ps)) { + case EJS_STRING_ROPE: kids[0] = ps->data.rope.left; kids[1] = ps->data.rope.right; break; + case EJS_STRING_DEPENDENT: kids[0] = ps->data.dependent.dep; break; + default: break; + } + for (int k = 0; k < 2; k++) { + if (!kids[k] || !_ejs_gc_is_young(kids[k])) continue; + uint32_t ci; + PageInfo* pg = find_page_and_cell(kids[k], &ci); + if (!pg) continue; + if (_ejs_gc_is_forwarded(pg->page_start + (size_t)ci * pg->cell_size)) continue; + if (cell_is_black(pg->page_bitmap[ci])) continue; + _ejs_log ("EJS_GC_VERIFY: old primstr %p (type %d) child %d -> unpromoted young %p\n", + p, EJS_PRIMSTR_GET_TYPE(ps), k, (void*)kids[k]); + abort(); + } + } +} + +// EJS_GC_PARANOID: after every minor, walk roots + modules + all live +// heap cells and validate every traceable value: it must resolve to an +// allocated cell whose header carries exactly one scan-type bit. +// Catches corruption at the collection that minted it. +EJSBool gc_paranoid; +static const char* paranoid_ctx; +static GCObjectPtr paranoid_owner; +static void +paranoid_check_slot(ejsval* slot) +{ + ejsval v = *slot; + if (!EJSVAL_IS_TRACEABLE_IMPL(v)) return; + GCObjectPtr p = (GCObjectPtr)EJSVAL_TO_GCTHING_IMPL(v); + if (p == NULL) return; + uint32_t ci; + PageInfo* pg = find_page_and_cell(p, &ci); + const char* why = NULL; + if (!pg) return; // static atoms/primstrings live outside the heap + if (0) why = ""; + else if (!cell_is_allocated(pg, ci, pg->page_bitmap[ci])) why = "target cell free"; + else { + GCObjectHeader h = *(GCObjectHeader*)(pg->page_start + (size_t)ci * pg->cell_size); + uint32_t st = (uint32_t)(h & 0xf); + if (st != 1 && st != 2 && st != 4 && st != 8) why = "bad scan type"; + else if (_ejs_gc_is_forwarded(pg->page_start + (size_t)ci * pg->cell_size)) why = "target forwarded"; + } + if (why) { + GCObjectHeader oh = paranoid_owner ? *(GCObjectHeader*)paranoid_owner : 0; + const char* ocls = "?"; + if (paranoid_owner && (oh & EJS_SCAN_TYPE_OBJECT) && ((EJSObject*)paranoid_owner)->ops) + ocls = ((EJSObject*)paranoid_owner)->ops->class_name; + else if (paranoid_owner && (oh & EJS_SCAN_TYPE_CLOSUREENV)) ocls = ""; + else if (paranoid_owner && (oh & EJS_SCAN_TYPE_PRIMSTR)) ocls = ""; + _ejs_log ("EJS_GC_PARANOID [%s]: owner %p (class %s, hdr %llx) slot %p value %llx: %s\n", + paranoid_ctx, (void*)paranoid_owner, ocls, (unsigned long long)oh, + (void*)slot, (unsigned long long)v.asBits, why); + abort(); + } +} +static void +paranoid_check_object(GCObjectPtr p) +{ + paranoid_owner = p; + GCObjectHeader header = *(GCObjectHeader*)p; + if ((header & EJS_SCAN_TYPE_OBJECT) != 0) { + EJSObject* obj = (EJSObject*)p; + if (obj->ops != NULL) OP(obj,Scan)(obj, paranoid_check_slot); + } + else if ((header & EJS_SCAN_TYPE_CLOSUREENV) != 0) { + EJSClosureEnv* env = (EJSClosureEnv*)p; + for (uint32_t i = 0; i < env->length; i++) + paranoid_check_slot(&env->slots[i]); + } + else if ((header & EJS_SCAN_TYPE_PRIMSYM) != 0) + paranoid_check_slot(&((EJSPrimSymbol*)p)->description); +} +void +paranoid_sweep_check(void) +{ + paranoid_ctx = "roots"; + root_registry_foreach (paranoid_check_slot); + paranoid_ctx = "modules"; + for (int i = 0; i < _ejs_num_modules; i++) { + EJSObject* mod = (EJSObject*)_ejs_modules[i]; + if (mod->ops) OP(mod,Scan)(mod, paranoid_check_slot); + } + paranoid_ctx = "oldgen"; + old_gen_walk (paranoid_check_object); + paranoid_ctx = "young"; + for (PageInfo* page = (PageInfo*)heap_priv.young_pages.head; page; page = page->next) { + GCObjectPtr p = page->page_start; + for (int c = 0; c < CELLS_IN_PAGE(page); c++, p += page->cell_size) { + EJSBool allocated = (page->young == 1) + ? young_cell_is_allocated(page, (uint32_t)c) + : !cell_is_free(page->page_bitmap[c]); + if (allocated && !_ejs_gc_is_forwarded(p)) + paranoid_check_object(p); + } + } +} + +void +_ejs_gc_dump_heap_stats() +{ + _ejs_log ("arenas:\n"); + for (int i = 0; i < num_arenas; i ++) { + _ejs_log (" [%d] - %p - %p\n", i, heap_arenas[i], heap_arenas[i]->end); + } + + for (int i = 0; i < HEAP_PAGELISTS_COUNT; i ++) { +#if gc_timings > 3 + EJSBool printed_something = EJS_FALSE; +#endif + _ejs_log ("heap_pages[%d, size %d] : %d pages\n", i, 1 << (i + OBJECT_SIZE_LOW_LIMIT_BITS), _ejs_list_length (&heap_pages[i])); +#if gc_timings > 3 + EJS_LIST_FOREACH (&heap_pages[i], PageInfo, page, { + GCObjectPtr p = page->page_start; + for (int c = 0; c < CELLS_IN_PAGE (page); c ++, p += page->cell_size) { + if (cell_is_free(page->page_bitmap[c])) + continue; + GCObjectHeader* headerp = (GCObjectHeader*)p; + if ((*headerp & EJS_SCAN_TYPE_OBJECT) != 0) _ejs_log ("O"); + else if ((*headerp & EJS_SCAN_TYPE_CLOSUREENV) != 0) _ejs_log ("C"); + else if ((*headerp & EJS_SCAN_TYPE_PRIMSTR) != 0) _ejs_log (((*headerp >> EJS_GC_USER_FLAGS_SHIFT) & 0x10) != 0 ? "s" : "S"); + else if ((*headerp & EJS_SCAN_TYPE_PRIMSYM) != 0) _ejs_log ("X"); + printed_something = EJS_TRUE; + } + }) + if (printed_something) + _ejs_log ("\n"); +#endif + } + + _ejs_log ("\n"); + +#if spew >= 2 + if (los_list) { + _ejs_log ("large object store: "); + for (LargeObjectInfo* lobj = los_list; lobj; lobj = lobj->next) { + GCObjectHeader* headerp = (GCObjectHeader*)lobj->page_info.page_start; + if ((*headerp & EJS_SCAN_TYPE_OBJECT) != 0) _ejs_log ("O"); + else if ((*headerp & EJS_SCAN_TYPE_CLOSUREENV) != 0) _ejs_log ("C"); + else if ((*headerp & EJS_SCAN_TYPE_PRIMSTR) != 0) _ejs_log ("S"); + else if ((*headerp & EJS_SCAN_TYPE_PRIMSYM) != 0) _ejs_log ("X"); + } + _ejs_log ("\n"); + } +#endif +} diff --git a/runtime/ejs-gc-heap.c b/runtime/ejs-gc-heap.c new file mode 100644 index 00000000..3edfee82 --- /dev/null +++ b/runtime/ejs-gc-heap.c @@ -0,0 +1,517 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=cpp: + */ + +// heap geography: the arena address-space reservation, arenas, page +// allocation, the large-object store and its sorted-range lookup, and +// find_page_and_cell — the pointer->cell resolution every scan uses. + +#include "ejs-gc-internal.h" + +#ifndef MAP_NORESERVE +#define MAP_NORESERVE 0 +#endif + +// GC-heap pointers get NaN-boxed into a 47-bit ejsval payload, so every +// page must map below 2^47. macOS hands out low addresses naturally; +// linux (48-bit VA, top-down mmap) does not — ask for a hinted region +// and bump the hint as regions fill. +static void* +mmap_boxable(size_t size, int prot, int extra_flags) +{ +#ifdef TARGET_LINUX + static uintptr_t hint = 0x280000000000UL; // well below 2^47 + for (int tries = 0; tries < 64; tries++) { + void* res = mmap((void*)hint, size, prot, MAP_ANON | MAP_PRIVATE | extra_flags, MAP_FD, 0); + if (res == MAP_FAILED) return NULL; + if (((uintptr_t)res + size) < (1UL << 47)) { + hint = (uintptr_t)res + size; + return res; + } + // unboxable address: drop it and try a fresh hint + munmap(res, size); + hint += 0x100000000UL; // 4GB stride + } + return NULL; +#else + void* res = mmap(NULL, size, prot, MAP_ANON | MAP_PRIVATE | extra_flags, MAP_FD, 0); + return res == MAP_FAILED ? NULL : res; +#endif +} + +static void* +alloc_from_os(size_t size) +{ + size = MAX(size, PAGE_SIZE); + void* res = mmap_boxable(size, PROT_READ | PROT_WRITE, 0); + SPEW(2, _ejs_log ("mmap = %p\n", res)); + return res; +} + +static void +release_to_os(void* ptr, size_t size) +{ + munmap (ptr, size); +} + +Arena *heap_arenas[MAX_ARENAS]; +int num_arenas; + +// ---- the arena address-space reservation (gc-P4) ---------------- +// +// All arenas are carved out of ONE contiguous reservation, mapped +// PROT_NONE at init and committed ARENA_SIZE at a time. Two payoffs, +// both for the conservative scanner: +// +// - the arena span is FIXED and disjoint from the C/LLVM heap for the +// life of the process. Before this, each arena was its own mmap: +// once a late arena landed beyond the C heap, the conservative +// prefilter span swallowed every malloc'd address, and during +// codegen MILLIONS of stack words pointing into LLVM's own +// allocations passed the prefilter into a per-word bsearch — the +// bistable 6s-vs-60s self-compile (mmap layout luck decided). +// - arena lookup is two compares + a shift into a direct map instead +// of a bsearch per candidate word. +// +// Reserved address space costs nothing until committed; nothing foreign +// can ever be mapped inside the reservation. +#define ARENA_SHIFT 25 +_Static_assert((1L << ARENA_SHIFT) == ARENA_SIZE, "ARENA_SHIFT matches ARENA_SIZE"); + +static char* arena_space; // base, ARENA_SIZE-aligned +static char* arena_space_pos; // next uncommitted chunk +static char* arena_space_end; // base + MAX_HEAP_SIZE +static Arena* arena_map[MAX_ARENAS]; // direct map: (ptr - base) >> ARENA_SHIFT + +static void +arena_space_reserve(void) +{ + size_t size = (size_t)MAX_HEAP_SIZE; + char* res = mmap_boxable(size + ARENA_SIZE, PROT_NONE, MAP_NORESERVE); + if (res == NULL) { + _ejs_log ("gc: unable to reserve the arena address space\n"); + abort(); + } + char* aligned = (char*)EJS_ALIGN(res, ARENA_SIZE); + // trim the alignment slop so the reservation is exactly the span + if (aligned > res) + munmap (res, aligned - res); + if (aligned + size < res + size + ARENA_SIZE) + munmap (aligned + size, (res + size + ARENA_SIZE) - (aligned + size)); + arena_space = aligned; + arena_space_pos = aligned; + arena_space_end = aligned + size; +} + +static inline Arena* +arena_lookup(GCObjectPtr ptr) +{ + uintptr_t off = (uintptr_t)((char*)ptr - arena_space); + if (off >= (uintptr_t)MAX_HEAP_SIZE) return NULL; + return arena_map[off >> ARENA_SHIFT]; +} + +// conservative-scan prefilter: [conservative_lo, conservative_hi) bounds +// every GC-managed address (the arena reservation + LOS blocks). The +// stack scanners reject candidate words with two compares before any +// lookup. Bounds only ever widen — stale coverage of freed LOS blocks +// is merely conservative, and a candidate inside the reservation that +// hits no committed arena rejects in the direct map. +char *conservative_lo = (char*)UINTPTR_MAX; +char *conservative_hi = NULL; +static inline void +conservative_bounds_add(void* start, size_t size) +{ + if ((char*)start < conservative_lo) conservative_lo = (char*)start; + if ((char*)start + size > conservative_hi) conservative_hi = (char*)start + size; +} + +// ---- LOS lookup: sorted range array ----------------------------- +// +// A conservative candidate that misses the arena reservation resolves +// against the LOS by binary search over a sorted array of payload +// ranges. This replaces a LOCKED LINEAR WALK of the whole LOS list — +// per stack word — which, with blocks scattered by mmap, could put +// hundreds of ms per pin scan on deep-recursion minors (found while +// gating sinking-P3; the [los_lo, los_hi) bounds prefilter landed then +// as a stopgap and remains as the quick reject). +static char *los_lo = (char*)UINTPTR_MAX; +static char *los_hi = NULL; + +EJSList heap_pages[HEAP_PAGELISTS_COUNT]; +LargeObjectInfo *los_list; + +// ---- LOS lookup: sorted range array ----------------------------- +// +// A conservative candidate that misses the arena reservation resolves +// against the LOS by binary search over a sorted array of payload +// ranges. This replaces a LOCKED LINEAR WALK of the whole LOS list — +// per stack word — which, with blocks scattered by mmap, could put +// hundreds of ms per pin scan on deep-recursion minors (found while +// gating sinking-P3; the [los_lo, los_hi) bounds prefilter landed then +// as a stopgap and remains as the quick reject). +typedef struct { + char* start; // payload: page_info.page_start + char* end; // start + cell_size + LargeObjectInfo* lobj; +} LOSRange; +static LOSRange* los_ranges; +static int los_range_count; +static int los_range_capacity; + +// index of the first range with start > ptr, in [0, count] +static int +los_range_upper_bound(char* ptr) +{ + int lo = 0, hi = los_range_count; + while (lo < hi) { + int mid = (lo + hi) / 2; + if (los_ranges[mid].start <= ptr) lo = mid + 1; + else hi = mid; + } + return lo; +} + +static void +los_ranges_add(LargeObjectInfo* lobj) +{ + char* start = (char*)lobj->page_info.page_start; + if (start < los_lo) los_lo = start; + if (start + lobj->page_info.cell_size > los_hi) + los_hi = start + lobj->page_info.cell_size; + + if (los_range_count == los_range_capacity) { + los_range_capacity = los_range_capacity ? los_range_capacity * 2 : 256; + los_ranges = realloc (los_ranges, los_range_capacity * sizeof(LOSRange)); + } + int at = los_range_upper_bound(start); + memmove (&los_ranges[at + 1], &los_ranges[at], + (los_range_count - at) * sizeof(LOSRange)); + los_ranges[at].start = start; + los_ranges[at].end = start + lobj->page_info.cell_size; + los_ranges[at].lobj = lobj; + los_range_count++; +} + +static void +los_ranges_remove(LargeObjectInfo* lobj) +{ + char* start = (char*)lobj->page_info.page_start; + int at = los_range_upper_bound(start) - 1; + EJS_ASSERT(at >= 0 && los_ranges[at].lobj == lobj); + memmove (&los_ranges[at], &los_ranges[at + 1], + (los_range_count - at - 1) * sizeof(LOSRange)); + los_range_count--; +} + +// interior pointers match: a conservative reference may be a derived +// pointer whose base value the optimizer discarded — with an exact-base +// match a large object referenced ONLY through an interior pointer +// (e.g. a flat string's data) would be collected out from under it. +// Callers canonicalize through cell_idx 0, so an interior hit marks the +// base. +static PageInfo* +los_lookup(GCObjectPtr ptr, uint32_t *cell_idx) +{ + if ((char*)ptr < los_lo || (char*)ptr >= los_hi) + return NULL; + int at = los_range_upper_bound((char*)ptr) - 1; + if (at < 0 || (char*)ptr >= los_ranges[at].end) + return NULL; + if (cell_idx) + *cell_idx = 0; + return &los_ranges[at].lobj->page_info; +} + +void* ptr_to_arena(void* ptr) { return PTR_TO_ARENA(ptr); } +void* ptr_to_arena_page_base(void* ptr) { return PTR_TO_ARENA_PAGE_BASE(ptr); } +uintptr_t ptr_to_arena_page_index(void* ptr) { return PTR_TO_ARENA_PAGE_INDEX(ptr); } +uintptr_t ptr_to_cell(void* ptr, PageInfo* info ) { return PTR_TO_CELL(ptr, info); } + +#if sanity +static void +verify_arena(Arena *arena) +{ + for (int i = 0; i < arena->num_pages; i ++) { + EJS_ASSERT (arena->pages[i] == arena->page_infos[i]->page_start); + } +} +#endif + + +Arena* +arena_new() +{ + if (arena_space_pos == arena_space_end) + return NULL; // the reservation IS the heap cap + + SPEW(1, _ejs_log ("num_arenas = %d, max = %d\n", num_arenas, MAX_ARENAS)); + + void* arena_start = arena_space_pos; + if (mprotect (arena_start, ARENA_SIZE, PROT_READ | PROT_WRITE) != 0) + return NULL; + + Arena* new_arena = arena_start; + + memset (new_arena, 0, sizeof(Arena)); + + new_arena->end = arena_start + ARENA_SIZE; + new_arena->pos = (void*)EJS_ALIGN(arena_start + sizeof(Arena), PAGE_SIZE); + + LOCK_ARENAS(); + arena_space_pos += ARENA_SIZE; + // sequential carving: heap_arenas stays address-sorted by construction + heap_arenas[num_arenas++] = new_arena; + arena_map[((char*)arena_start - arena_space) >> ARENA_SHIFT] = new_arena; + UNLOCK_ARENAS(); + + return new_arena; +} + +// one reservation holds every arena the process will ever commit; the +// conservative prefilter covers it from day one (candidates in +// uncommitted space reject via the direct map) +void +heap_space_init(void) +{ + arena_space_reserve(); + conservative_bounds_add (arena_space, (size_t)MAX_HEAP_SIZE); + + // allocate an initial arenas + for (int i = 0; i < 10; i ++) + arena_new(); +} + +static PageInfo* +alloc_page_info_from_arena(Arena *arena, void *page_data, size_t cell_size) +{ + // FIXME allocate the PageInfo and bitmap from the arena as well + PageInfo* info = (PageInfo*)calloc(1, sizeof(PageInfo) + (sizeof(BitmapCell) * PAGE_SIZE / (1<cell_size = cell_size; + info->num_cells = CELLS_OF_SIZE(cell_size); + info->num_free_cells = info->num_cells; + EJS_ASSERT(info->num_cells > 0); + info->page_start = page_data; + info->page_end = info->page_start + PAGE_SIZE; + // allocate a bitmap large enough to store any sized object so we can reuse the bitmap + info->page_bitmap = (BitmapCell*)(((char*)info) + sizeof(PageInfo)); + info->bump_ptr = info->page_start; + memset (info->page_bitmap, CELL_FREE, info->num_cells * sizeof(BitmapCell)); + return info; +} + +PageInfo* +alloc_page_from_arena(Arena *arena, size_t cell_size) +{ + void *page_data = (void*)EJS_ALIGN(arena->pos, PAGE_SIZE); + if (arena->free_pages) { + PageInfo* info = arena->free_pages; + EJS_LIST_DETACH(info, arena->free_pages); + info->cell_size = cell_size; + info->num_cells = CELLS_OF_SIZE(cell_size); + info->num_free_cells = info->num_cells; + info->bump_ptr = info->page_start; + memset (info->page_bitmap, CELL_FREE, info->num_cells * sizeof(BitmapCell)); + SPEW(3, _ejs_log ("alloc_page_from_arena from free pages for cell size %zd = %p\n", info->cell_size, info)); + return info; + } + else if (page_data < arena->end) { + PageInfo* info = alloc_page_info_from_arena (arena, page_data, cell_size); + int page_idx = arena->num_pages++; + arena->pos = page_data + PAGE_SIZE; + arena->pages[page_idx] = page_data; + arena->page_infos[page_idx] = info; + SPEW(3, _ejs_log ("alloc_page_from_arena from bump pointer for cell size %zd = %p\n", info->cell_size, info)); + return info; + } + else { + return NULL; + } +} + +PageInfo* +find_page_and_cell(GCObjectPtr ptr, uint32_t *cell_idx) +{ + // bounds prefilter: static data (atoms, module structs) and foreign + // pointers reject in two compares + if ((char*)ptr < conservative_lo || (char*)ptr >= conservative_hi) + return NULL; + + Arena* arena = arena_lookup(ptr); + if (EJS_LIKELY (arena != NULL)) { + SANITY(verify_arena(arena)); + + int page_index = PTR_TO_ARENA_PAGE_INDEX(ptr); + + if (page_index < 0 || page_index >= arena->num_pages) { + return NULL; + } + + PageInfo *page = arena->page_infos[page_index]; + + // note: interior pointers are accepted (PTR_TO_CELL divides by the + // cell size, so any pointer into a cell resolves to that cell). + // optimized code compiled by ejs keeps addresses of closure env + // slots live across calls with the env base pointer dead, so the + // conservative scan must treat interior pointers as referencing + // the containing object. + + if (cell_idx) { + *cell_idx = PTR_TO_CELL(ptr, page); + EJS_ASSERT(*cell_idx >= 0 && *cell_idx < CELLS_IN_PAGE(page)); + } + + return page; + } + + return los_lookup(ptr, cell_idx); +} + +PageInfo* +alloc_new_page(size_t cell_size) +{ + EJS_ASSERT(cell_size >= (1 << OBJECT_SIZE_LOW_LIMIT_BITS)); + SPEW(2, _ejs_log ("allocating new page for cell size %zd\n", cell_size)); + PageInfo *rv = NULL; + for (int i = 0; i < num_arenas; i ++) { + // nursery arenas serve young allocation only + if (heap_arenas[i]->is_nursery) + continue; + rv = alloc_page_from_arena(heap_arenas[i], cell_size); + if (rv) { + SPEW(2, _ejs_log (" => %p", rv)); + return rv; + } + } + + // need a new arena + SPEW(2, _ejs_log ("unable to find page in current arenas, allocating a new one")); + LOCK_ARENAS(); + Arena* arena = arena_new(); + UNLOCK_ARENAS(); + if (arena == NULL) + return NULL; + rv = alloc_page_from_arena(arena, cell_size); + SPEW(2, _ejs_log (" => %p", rv)); + return rv; +} + +// walk every live OLD cell (arena pages + LOS), calling `fn` on the +// object — the remset-overflow fallback and the EJS_GC_VERIFY check +void +old_gen_walk(void (*fn)(GCObjectPtr)) +{ + for (int a = 0; a < num_arenas; a++) { + Arena* arena = heap_arenas[a]; + if (!arena || arena->is_nursery) continue; + for (int pg = 0; pg < arena->num_pages; pg++) { + PageInfo* info = arena->page_infos[pg]; + if (!info || info->young) continue; + GCObjectPtr p = info->page_start; + for (int c = 0; c < CELLS_IN_PAGE(info); c++, p += info->cell_size) { + if (cell_is_free(info->page_bitmap[c])) continue; + fn (p); + } + } + } + for (LargeObjectInfo* lobj = los_list; lobj; lobj = lobj->next) { + if (cell_is_free(lobj->page_info.page_bitmap[0])) continue; + fn (lobj->page_info.page_start); + } +} + +size_t +calc_heap_size() +{ + size_t size = 0; + for (int hp = 0; hp < HEAP_PAGELISTS_COUNT; hp++) { + size += _ejs_list_length(&heap_pages[hp]) * PAGE_SIZE; + } + return size; +} + +GCObjectPtr +alloc_from_page(PageInfo *info) +{ + LOCK_PAGE(info); + + EJS_ASSERT (info->num_free_cells > 0); + + GCObjectPtr rv = NULL; + uint32_t cell; + + SPEW(2, _ejs_log ("allocating object from page %p (cell size %zd)\n", info, info->cell_size)); + + if (info->bump_ptr) { + rv = (GCObjectPtr)EJS_ALIGN(info->bump_ptr, 8); + cell = PTR_TO_CELL(info->bump_ptr, info); + info->bump_ptr += info->cell_size; + // check if we can service the next alloc request from the bump_ptr. if we can't, switch + // to the freelist code below. + if (info->bump_ptr + info->cell_size >= info->page_end) + info->bump_ptr = NULL; + } + else { + for (cell = 0; cell < info->num_cells; cell ++) { + if (cell_is_free(info->page_bitmap[cell])) { + rv = info->page_start + (cell * info->cell_size); + break; + } + } + } + + EJS_ASSERT (rv); + + cell_set_allocated(&info->page_bitmap[cell]); + cell_set_white(&info->page_bitmap[cell]); + + info->num_free_cells --; + + UNLOCK_PAGE(info); + + SPEW(2, _ejs_log ("allocated obj %p from page %p (cell size %zd), free cells remaining %zd\n", rv, info, info->cell_size, info->num_free_cells)); + +#if !clear_on_finalize + memset(rv, 0, info->cell_size); +#endif + return rv; +} + +GCObjectPtr +alloc_from_los(size_t size, EJSScanType scan_type) +{ + // allocate enough space for the object, our header, and our bitmap. leave room enough to align the return value + LargeObjectInfo *rv = alloc_from_os(size + sizeof(LargeObjectInfo) + 16); + if (rv == NULL) + return NULL; + + rv->page_info.page_bitmap = (char*)((void*)rv + sizeof(LargeObjectInfo)); // our bitmap comes right after the header + rv->page_info.page_start = (void*)EJS_ALIGN((void*)rv + sizeof(LargeObjectInfo) + 8, 8); + rv->page_info.cell_size = size; + rv->page_info.num_cells = 1; + rv->page_info.num_free_cells = 0; + rv->page_info.los_info = rv; + + cell_set_white(&rv->page_info.page_bitmap[0]); + cell_set_allocated(&rv->page_info.page_bitmap[0]); + + *((GCObjectHeader*)rv->page_info.page_start) = scan_type | EJS_GC_HEADER_YOUNG; + + rv->alloc_size = size; + + conservative_bounds_add (rv, size + sizeof(LargeObjectInfo) + 16); + los_ranges_add (rv); + EJS_LIST_PREPEND (rv, los_list); + //_ejs_log ("alloc_from_los returning %p\n, los_list = %p\n", rv->page_info.page_start, los_list); + return rv->page_info.page_start; +} + +void +release_to_los (LargeObjectInfo *lobj) +{ + los_ranges_remove (lobj); + // the mapping covers the header + bitmap slop too, not just the + // payload (releasing only alloc_size leaked the tail page) + release_to_os (lobj, lobj->alloc_size + sizeof(LargeObjectInfo) + 16); +} diff --git a/runtime/ejs-gc-internal.h b/runtime/ejs-gc-internal.h new file mode 100644 index 00000000..d5342067 --- /dev/null +++ b/runtime/ejs-gc-internal.h @@ -0,0 +1,388 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=cpp: + */ + +// The collector's internal contract (runtime-P4 / P6.3 file split). +// Nothing here is API — ejs-gc.h is the public surface. Module map: +// +// ejs-gc.c lifecycle API, allocator entry, cell free path, +// root registry, collection policy, GC JS object +// ejs-gc-heap.c arena reservation, arenas/pages, LOS + lookup, +// find_page_and_cell +// ejs-gc-mark.c worklist, precise + conservative scanners, +// gc-frame skip, generator stack bookkeeping +// ejs-gc-minor.c the nursery and the mostly-copying minor +// ejs-gc-major.c full collections: mark/sweep orchestration, +// major compaction, the epoch advance +// ejs-gc-debug.c EJS_GC_PROFILE / WATCH / VERIFY / PARANOID + +#ifndef _ejs_gc_internal_h_ +#define _ejs_gc_internal_h_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ejs-gc.h" +#include "ejs-function.h" +#include "ejs-generator.h" +#include "ejs-arguments.h" +#include "ejs-shapes.h" +#include "ejs-value.h" +#include "ejs-string.h" +#include "ejs-symbol.h" +#include "ejs-error.h" +#include "ejs-ops.h" +#include "ejsval.h" +#include "ejs-module.h" + +#define clear_on_finalize 0 + +#define spew 0 +#define sanity 0 +#define gc_timings 0 + +#if spew +static int _ejs_spew_level = (spew); +#define SPEW(level,x) do { if ((level) < _ejs_spew_level) { x; } } while (0) +#else +#define SPEW(level,x) +#endif +#if sanity +#define SANITY(x) x +#else +#define SANITY(x) +#endif + +#if EJS_BITS_PER_WORD == 64 +// 2GB +#define MAX_HEAP_SIZE (2LL * 1024LL * 1024LL * 1024LL) +#else +// 128MB +#define MAX_HEAP_SIZE (128LL * 1024LL * 1024LL) +#endif + +#ifndef PAGE_SIZE +#define PAGE_SIZE 4096 +#endif + +#define USABLE_PAGE_SIZE PAGE_SIZE + +#define CELLS_OF_SIZE(size) (USABLE_PAGE_SIZE / (size)) +#define CELLS_IN_PAGE(page) CELLS_OF_SIZE((page)->cell_size) + +// arenas are reserved in ARENA_PAGES * PAGE_SIZE chunks. ARENA_PAGES=8192 gives us an arena size of 32MB +#define ARENA_PAGES 8192 +#define ARENA_SIZE (PAGE_SIZE*ARENA_PAGES) + +#define PTR_TO_ARENA_MASK (uintptr_t)(~(ARENA_SIZE-1)) + +// turn a random pointer into an arena pointer +#define PTR_TO_ARENA(ptr) ((void*)((uintptr_t)(ptr) & PTR_TO_ARENA_MASK)) +#define PTR_TO_ARENA_PAGE_BASE(ptr) ((void*)EJS_ALIGN(PTR_TO_ARENA(ptr) + sizeof(Arena), PAGE_SIZE)) +#define PTR_TO_ARENA_PAGE_INDEX(ptr) ((((uintptr_t)(ptr) & ~PTR_TO_ARENA_MASK) - ((uintptr_t)PTR_TO_ARENA_PAGE_BASE(ptr) & ~PTR_TO_ARENA_MASK)) / PAGE_SIZE) + +#define PTR_TO_CELL(ptr,info) (((char*)(ptr) - (char*)(info)->page_start) / (info)->cell_size) + +#define IS_ALIGNED_TO(v,a) (((uintptr_t)(v) & ((a)-1)) == 0) +#define ALLOC_ALIGN 8 +#define EJS_ALIGN(v,a) (((uintptr_t)(v) + (a)-1) & ~((a)-1)) +#define IS_ALLOC_ALIGNED(v) IS_ALIGNED_TO(v, ALLOC_ALIGN) + +#if IOS || OSX +#include +#define MAP_FD VM_MAKE_TAG (VM_MEMORY_APPLICATION_SPECIFIC_16) +#else +#define MAP_FD -1 +#endif + +// two header bits from the gc-reserved range (57-63; see ejs-types.h). +// YOUNG: set at allocation, cleared on first survival (profiling) or +// promotion (the nursery). PINNED: set on every conservative hit during +// a full collection — the compacting major must sweep that cell in +// place; cleared by compaction's fixup walk (or the profile census when +// compaction is off). +#define EJS_GC_HEADER_YOUNG (1ULL << 57) +#define EJS_GC_HEADER_PINNED (1ULL << 58) + +#if CONCURRENT +#error "not implemented" +#else +#define LOCK_PAGE(info) +#define UNLOCK_PAGE(info) +#define LOCK_GC() +#define UNLOCK_GC() +#define LOCK_ARENAS() +#define UNLOCK_ARENAS() +#endif + +typedef struct _PageInfo PageInfo; +typedef struct _LargeObjectInfo LargeObjectInfo; + +typedef struct _Arena { + void* end; + void* pos; + PageInfo* free_pages; + void* pages[ARENA_PAGES]; + PageInfo* page_infos[ARENA_PAGES]; + int num_pages; + // the nursery is a dedicated arena so "is young" is a + // range check; old-gen page allocation skips nursery arenas + EJSBool is_nursery; +} Arena; + +#define MAX_ARENAS (MAX_HEAP_SIZE / ARENA_SIZE) + +// ---- the cell lifecycle ---------------------------------------- +// +// One bitmap byte per page cell. A cell is FREE or ALLOCATED, and an +// allocated cell carries a tri-color mark; every state predicate and +// transition lives in this block, and the encoding is private to it. +// +// White/black are EPOCH-RELATIVE: the color bits hold GRAY or the +// parity of the mark epoch the cell was last colored in. color == +// (mark_epoch & 1) is black (marked this epoch); the complement is +// white. mark_epoch_advance() — called at exactly one site, the end +// of a full collection — thus turns every surviving black cell white +// in O(1) without touching a bitmap. (The old collector expressed +// the same aging as a white_mask/black_mask swap mutated at the same +// site; the epoch is that flip made explicit and single-owner.) + +typedef char BitmapCell; + +#define CELL_COLOR_MASK 0x03 +#define CELL_GRAY 0x02 +#define CELL_FREE 0x04 // cell is in the free list for this page + +extern unsigned int mark_epoch; // parity 1 at startup: black starts at color 1 + +static inline BitmapCell cell_black_color(void) { return (BitmapCell)(mark_epoch & 1); } +static inline BitmapCell cell_white_color(void) { return (BitmapCell)((mark_epoch & 1) ^ 1); } + +// the ONLY place the white/black meaning ever changes +static inline void +mark_epoch_advance(void) +{ + mark_epoch++; +} + +static inline EJSBool cell_is_free (BitmapCell c) { return (c & CELL_FREE) == CELL_FREE; } +static inline EJSBool cell_is_gray (BitmapCell c) { return (c & CELL_COLOR_MASK) == CELL_GRAY; } +static inline EJSBool cell_is_white(BitmapCell c) { return (c & CELL_COLOR_MASK) == cell_white_color(); } +static inline EJSBool cell_is_black(BitmapCell c) { return (c & CELL_COLOR_MASK) == cell_black_color(); } + +static inline void cell_set_gray (BitmapCell* c) { *c = (BitmapCell)((*c & ~CELL_COLOR_MASK) | CELL_GRAY); } +static inline void cell_set_white(BitmapCell* c) { *c = (BitmapCell)((*c & ~CELL_COLOR_MASK) | cell_white_color()); } +static inline void cell_set_black(BitmapCell* c) { *c = (BitmapCell)((*c & ~CELL_COLOR_MASK) | cell_black_color()); } +static inline void cell_set_free (BitmapCell* c) { *c = CELL_FREE; } +static inline void cell_set_allocated(BitmapCell* c) { *c = (BitmapCell)(*c & ~CELL_FREE); } + +struct _PageInfo { + EJS_LIST_HEADER(struct _PageInfo); + void* bump_ptr; + void* page_start; + void* page_end; + BitmapCell* page_bitmap; + LargeObjectInfo *los_info; + int32_t cell_size; + int16_t num_cells; + int16_t num_free_cells; + // 0 = old gen; 1 = active young page (bump-allocated, + // allocated-ness = below bump); 2 = young survivor page (holds + // pinned young objects, bitmap-authoritative, no further bumping) + uint8_t young; +}; + +struct _LargeObjectInfo { + EJS_LIST_HEADER(struct _LargeObjectInfo); + size_t alloc_size; + PageInfo page_info; +}; + +#define OBJECT_SIZE_LOW_LIMIT_BITS 4 // smallest object we'll allocate (1<<4 = 16) +#define OBJECT_SIZE_HIGH_LIMIT_BITS 8 // max object size for the non-LOS allocator = 256 + +// heap_pages is indexed by ffs(cell_size) - OBJECT_SIZE_LOW_LIMIT_BITS, +// i.e. 16B -> 1 .. 256B -> 5 ([0] is unused); +2 covers the inclusive +// top class. Until gc-P5 the ffs comparisons routed 256-byte +// cells to the LOS (ffs(256) = 9 > HIGH_LIMIT_BITS), so the top class +// existed only on paper — the pre-gc-P4 LOS had a linear lookup that +// made large cell populations quadratic to mark. With the LOS bsearch +// and the direct arena map in, the class is enabled: single-cell shaped +// objects up to the 14-field cap (32+16+112 = 160) and >14-slot envs +// now take pages, not the LOS. +#define HEAP_PAGELISTS_COUNT (OBJECT_SIZE_HIGH_LIMIT_BITS - OBJECT_SIZE_LOW_LIMIT_BITS) + 2 + +// allocated-ness of a young ACTIVE page's cell is the bump rule: +// everything below the bump cursor is an object, the bitmap holds only +// collection colors +static inline EJSBool +young_cell_is_allocated(PageInfo* page, uint32_t cell_idx) +{ + return page->page_start + (size_t)cell_idx * page->cell_size < page->bump_ptr; +} + +// allocated-ness of a cell: old pages answer from the bitmap; ACTIVE +// young pages (young==1) answer from the bump rule; SURVIVOR young +// pages (young==2) are bitmap-authoritative again (their pinned cells +// were re-marked at minor sweep) +static inline EJSBool +cell_is_allocated(PageInfo* page, uint32_t cell_idx, BitmapCell cell) +{ + if (page->young == 1) return young_cell_is_allocated(page, cell_idx); + return !cell_is_free(cell); +} + +// rewrite an ejsval's payload in place, preserving its NaN-box tag +static inline void +rewrite_slot_payload(ejsval* slot, GCObjectPtr to) +{ + slot->asBits = (slot->asBits & ~EJSVAL_PAYLOAD_MASK) + | ((uint64_t)(uintptr_t)to & EJSVAL_PAYLOAD_MASK); +} + +// the private half of the (single) isolate's heap context (_ejs_heap +// in ejs-gc.h is the emitted-code seam; this is everything else) +typedef struct { + Arena* nursery_arena; + PageInfo* young_current[EJS_GC_NUM_SIZE_CLASSES]; + EJSList young_pages; // all young pages not currently being bumped + EJSBool verify; // EJS_GC_VERIFY: old-gen barrier-coverage check per minor + size_t young_alloced; // bytes of young pages handed out this cycle + size_t young_budget; // minor-collection trigger (EJS_GC_NURSERY_BUDGET) + // minor worklist (objects whose slots still need processing) + GCObjectPtr* wl; + int wl_count, wl_cap; + // the remset's second buffer. A minor collection SWAPS buffers up + // front and processes the snapshot; slots whose referent stays young + // (pinned) re-append into the live buffer — old→young edges CARRY + // across cycles for as long as the target remains in the nursery. + void** remset_other; + // stats (reported under EJS_GC_PROFILE) + uint64_t minors, minor_usec_total, minor_usec_max; + uint64_t promoted_objs, promoted_bytes, minor_pins, remset_peak, overflow_minors; +} EJSHeapPriv; + +// conservative-pin attribution for EJS_GC_PROFILE +enum { + PROF_SRC_CSTACK = 0, // conservative C-stack ranges (incl. suspended segments) + PROF_SRC_REGS = 1, // spilled register file + PROF_SRC_GENSTACK = 2, // suspended generator stacks + saved ucontexts + PROF_SRC_COUNT +}; + +// ---- the collection policy (ejs-gc.c) --------------------------- +typedef enum { + GC_POLICY_YOUNG_ALLOC, // a nursery allocation is about to run + GC_POLICY_OLD_ALLOC, // an old-gen/LOS allocation is about to run + GC_POLICY_AFTER_MINOR, // a minor just retired; promotions grew the old gen + GC_POLICY_ALLOC_FAILED // allocator out of memory: forced full +} GCPolicyEvent; + +void gc_policy(GCPolicyEvent ev, const char* reason); + +// ---- shared state ---------------------------------------------- + +// mode/knob flags +extern EJSBool gc_disabled; // EJS_GC_DISABLE (ejs-gc.c) +extern int collect_every_alloc; // EJS_GC_EVERY_N_ALLOC (ejs-gc.c) +extern EJSBool compact_enabled; // EJS_GC_COMPACT (ejs-gc.c) +extern EJSBool nursery_enabled; // EJS_GC_NURSERY (ejs-gc-minor.c) +extern EJSBool gc_profile; // EJS_GC_PROFILE (ejs-gc-debug.c) +extern EJSBool gc_paranoid; // EJS_GC_PARANOID (ejs-gc-debug.c) +extern uintptr_t gc_watch_addr; // EJS_GC_WATCH (ejs-gc-debug.c) + +// allocator accounting (ejs-gc.c) +extern size_t alloc_size; // old-gen bytes ever allocated (promotions included) +extern size_t alloc_size_at_last_gc; +extern int num_allocs; // the every-N stress counter +extern int total_allocs; + +// heap geography (ejs-gc-heap.c) +extern EJSList heap_pages[]; +extern LargeObjectInfo *los_list; +extern Arena *heap_arenas[]; +extern int num_arenas; +extern char *conservative_lo; // conservative-scan prefilter bounds +extern char *conservative_hi; + +// collection state +extern EJSBool in_minor_gc; // (ejs-gc-minor.c) +extern EJSHeapPriv heap_priv; // (ejs-gc-minor.c) +extern EJSBool minor_scan_saw_young; // (ejs-gc-minor.c) set when a scan leaves a pinned-young referent +extern size_t heap_size_at_last_gc; // (ejs-gc-major.c) post-sweep footprint, drives full_gc_trigger +extern int num_roots; // (ejs-gc-major.c) per-cycle census counter +extern GCObjectPtr *stack_bottom; // (ejs-gc-mark.c) + +// profiling state written outside ejs-gc-debug.c +extern struct timeval prof_start_tv; +extern int prof_pin_source; // PROF_SRC_*, set by the scanners +extern const char* prof_gc_reason; + +// ---- cross-module functions ------------------------------------ + +// ejs-gc.c +void root_registry_foreach(void (*fn)(ejsval*)); +void root_registry_shutdown(void); +void finalize_object(GCObjectPtr p); +void _ejs_finalize_obj(GCObjectPtr ptr, Arena* arena, PageInfo* info, uint32_t cell_idx); + +// ejs-gc-heap.c +void heap_space_init(void); +Arena* arena_new(void); +PageInfo* alloc_page_from_arena(Arena *arena, size_t cell_size); +PageInfo* find_page_and_cell(GCObjectPtr ptr, uint32_t *cell_idx); +PageInfo* alloc_new_page(size_t cell_size); +GCObjectPtr alloc_from_page(PageInfo *info); +GCObjectPtr alloc_from_los(size_t size, EJSScanType scan_type); +void release_to_los(LargeObjectInfo *lobj); +void old_gen_walk(void (*fn)(GCObjectPtr)); +size_t calc_heap_size(void); + +// ejs-gc-mark.c +void _ejs_gc_worklist_init(void); +void mark_thread_stack(void); +void mark_generator_stacks(void); +void mark_from_roots(void); +void mark_from_modules(void); +void mark_object_root(GCObjectPtr ptr); +void process_worklist(void); +void walk_gc_frames(void (*slot_fn)(ejsval*)); +void set_frame_skip_chain(void* chain_head); +void clear_frame_skip(void); + +// ejs-gc-minor.c +void _ejs_gc_minor_collect(const char* reason); +GCObjectPtr young_alloc_slow(int idx, size_t cell_size, EJSScanType scan_type); +void young_normalize_for_full_gc(void); +void young_page_freed(PageInfo* info, Arena* arena); +void nursery_init(void); +void remset_rebuild_after_full_gc(void); +void minor_conservative_hit(PageInfo* page, uint32_t cell_idx); +void minor_wl_push(GCObjectPtr p); +void minor_fixup_evacuated(GCObjectPtr from, GCObjectPtr to, size_t cell_size); + +// ejs-gc-major.c +void _ejs_gc_collect_inner(EJSBool shutting_down); + +// ejs-gc-debug.c +void profile_note_alloc(size_t size, int ffs_bucket, EJSScanType scan_type); +void profile_note_pin(PageInfo* page, uint32_t cell_idx, GCObjectPtr raw); +void profile_pre_sweep(void); +void profile_report_cycle_end(uint64_t pause_usec); +void profile_report_shutdown(void); +void gc_watch_hit(const char* what, void* p); +void paranoid_sweep_check(void); +int paranoid_report_referrers(GCObjectPtr p); +extern void** paranoid_stack_floor; // raw-stack sweep floor, set at minor entry +void verify_check_object(GCObjectPtr p); +extern ejsval* verify_bad_slot; +void _ejs_gc_dump_heap_stats(void); + +#endif /* _ejs_gc_internal_h_ */ diff --git a/runtime/ejs-gc-major.c b/runtime/ejs-gc-major.c new file mode 100644 index 00000000..abde799f --- /dev/null +++ b/runtime/ejs-gc-major.c @@ -0,0 +1,539 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=cpp: + */ + +// full collections: mark orchestration, the sweep, the mostly-copying +// major compaction (gc-P4), and the post-cycle epoch advance. + +#include "ejs-gc-internal.h" + +int num_roots = 0; +static int white_objs = 0; +static int large_objs = 0; +static int total_objs = 0; + +static void +sweep_heap() +{ +#if spew + int pages_visited = 0; + int pages_skipped = 0; +#endif + + // sweep the entire heap, freeing white nodes + for (int a = 0, e = num_arenas; a < e; a ++) { + Arena* arena = heap_arenas[a]; + + if (!arena) + continue; + + for (int p = 0, pe = arena->num_pages; p < pe; p++) { + PageInfo *info = arena->page_infos[p]; + + if (info->num_free_cells == info->num_cells) { +#if spew + pages_skipped++; +#endif + } + else { +#if spew + pages_visited ++; +#endif + + for (int c = 0, ce = info->num_cells; c < ce; c ++) { + BitmapCell cell = info->page_bitmap[c]; + + if (cell_is_free(cell)) + continue; + + total_objs++; + + if (cell_is_white(cell)) { + white_objs++; + + GCObjectPtr gcobj = (GCObjectPtr)(info->page_start + c * info->cell_size); + _ejs_finalize_obj(gcobj, arena, info, c); + } + } + } + } + } + + // sweep the large object store + SPEW(2, _ejs_log ("sweeping los: ")); + LargeObjectInfo *lobj = los_list; + while (lobj) { + large_objs ++; + PageInfo *info = &lobj->page_info; + BitmapCell cell = info->page_bitmap[0]; + LargeObjectInfo *next = lobj->next; + if (cell_is_white(cell)) { + // SPEW(2, { _ejs_log ("l"); fflush(stderr); }); + white_objs++; + + EJS_LIST_DETACH(lobj, los_list); + _ejs_finalize_obj(info->page_start, NULL, info, 0); + } + else { + // SPEW(2, { _ejs_log ("L"); fflush(stderr); }); + } + lobj = next; + } + SPEW(2, { _ejs_log ("\n"); }); +} + +// ============== mostly-copying major compaction (gc-P4) =================== +// +// Mark-sweep never shrinks: live old-gen cells sit wherever history put +// them and sparse pages hold whole pages hostage for a cell or two. +// After the sweep, this pass evacuates the live UNPINNED cells of the +// sparsest pages of each size class into the free space of the denser +// ones, rewrites every reference through the P1 forwarding records, and +// returns the emptied pages to their arenas — the heap actually shrinks, +// and the proportional growth target then adapts downward. +// +// Pinned cells sweep in place, exactly like the minor's young pins: +// conservative hits (C stack, spilled registers, generator stacks) set +// PINNED during marking, and every registered generator object pins too +// (the registry is an intrusive list of raw pointers). LOS objects +// never move. EJS_GC_COMPACT=off restores plain mark-sweep for A/B and +// differential runs. +static uint64_t compact_moved_objs, compact_moved_bytes, compact_freed_pages; + +static void +compact_fixup_slot(ejsval* slot) +{ + ejsval v = *slot; + if (!EJSVAL_IS_TRACEABLE_IMPL(v)) return; + GCObjectPtr p = (GCObjectPtr)EJSVAL_TO_GCTHING_IMPL(v); + if (p == NULL) return; + // boxed payloads are object bases, and statics outside the heap have + // headers too, so the forwarded-bit read is always safe + if (_ejs_gc_is_forwarded(p)) + rewrite_slot_payload(slot, _ejs_gc_forwarding_addr(p)); +} + +static void +compact_fixup_primstr_child(EJSPrimString** childp) +{ + GCObjectPtr p = (GCObjectPtr)*childp; + if (p && _ejs_gc_is_forwarded(p)) + *childp = (EJSPrimString*)_ejs_gc_forwarding_addr(p); +} + +static void +compact_fixup_object(GCObjectPtr p) +{ + GCObjectHeader* h = (GCObjectHeader*)p; + if (*h & EJS_GC_HEADER_FORWARDED) + return; // an evacuated source; its copy is walked on its own page + *h &= ~EJS_GC_HEADER_PINNED; // pins are per-cycle + if ((*h & EJS_SCAN_TYPE_OBJECT) != 0) { + EJSObject* obj = (EJSObject*)p; + if (obj->ops != NULL) + OP(obj,Scan)(obj, compact_fixup_slot); + } + else if ((*h & EJS_SCAN_TYPE_PRIMSTR) != 0) { + EJSPrimString* ps = (EJSPrimString*)p; + switch (EJS_PRIMSTR_GET_TYPE(ps)) { + case EJS_STRING_ROPE: + compact_fixup_primstr_child(&ps->data.rope.left); + compact_fixup_primstr_child(&ps->data.rope.right); + break; + case EJS_STRING_DEPENDENT: + compact_fixup_primstr_child(&ps->data.dependent.dep); + break; + case EJS_STRING_FLAT: + break; + } + } + else if ((*h & EJS_SCAN_TYPE_PRIMSYM) != 0) + compact_fixup_slot(&((EJSPrimSymbol*)p)->description); + else if ((*h & EJS_SCAN_TYPE_CLOSUREENV) != 0) { + EJSClosureEnv* env = (EJSClosureEnv*)p; + for (uint32_t i = 0; i < env->length; i++) + compact_fixup_slot(&env->slots[i]); + } +} + +static EJSBool +compact_page_has_pins(PageInfo* pg) +{ + GCObjectPtr p = pg->page_start; + for (int c = 0; c < pg->num_cells; c++, p += pg->cell_size) + if (!cell_is_free(pg->page_bitmap[c]) + && (*(GCObjectHeader*)p & EJS_GC_HEADER_PINNED)) + return EJS_TRUE; + return EJS_FALSE; +} + +// destination cell in `bucket`: first page (from the cursor on) with +// free capacity. Sources were detached from the bucket list before +// evacuation, so every listed page qualifies. The selection accounting +// guarantees capacity; running dry is a bug. +static GCObjectPtr +compact_alloc_dest(int bucket, PageInfo** cursor, PageInfo** dest_page) +{ + PageInfo* pg = *cursor ? *cursor : (PageInfo*)heap_pages[bucket].head; + while (pg && !pg->num_free_cells) + pg = pg->next; + if (!pg) { + _ejs_log ("GC BUG: compaction ran out of destination space (bucket %d)\n", bucket); + abort(); + } + *cursor = pg; + *dest_page = pg; + return alloc_from_page(pg); +} + +static void +compact_evacuate_page(int bucket, PageInfo* pg, PageInfo** cursor) +{ + GCObjectPtr from = pg->page_start; + for (int c = 0; c < pg->num_cells; c++, from += pg->cell_size) { + if (cell_is_free(pg->page_bitmap[c])) + continue; + PageInfo* dest_page; + GCObjectPtr to = compact_alloc_dest(bucket, cursor, &dest_page); + memcpy (to, from, pg->cell_size); + // the copy is live THIS cycle: keep it marked so the coming + // color flip turns it white with every other survivor + cell_set_black(&dest_page->page_bitmap[PTR_TO_CELL(to, dest_page)]); + minor_fixup_evacuated(from, to, pg->cell_size); + _ejs_gc_forward(from, to); + gc_watch_hit ("compact-evacuate-from", from); + compact_moved_objs++; + compact_moved_bytes += pg->cell_size; + } +} + +typedef struct { PageInfo* page; int live; } CompactPageStat; + +static int +compact_stat_cmp(const void* a, const void* b) +{ + return ((const CompactPageStat*)a)->live - ((const CompactPageStat*)b)->live; +} + +static void +compact_old_gen(void) +{ + // every registered generator pins: the registry reaches them through + // raw intrusive pointers (reg_next/reg_prev), and their machine + // state is re-scanned conservatively by their specops + for (EJSGenerator* g = _ejs_generator_registry; g; g = g->reg_next) + *(GCObjectHeader*)g |= EJS_GC_HEADER_PINNED; + + uint64_t moved_before = compact_moved_objs; + uint64_t freed_before = compact_freed_pages; + + EJSList evac_pages; + memset (&evac_pages, 0, sizeof(evac_pages)); + + // 1. selection + evacuation, per size class: sparse-first, evacuate + // while the rest of the class has room + for (int bucket = 0; bucket < HEAP_PAGELISTS_COUNT; bucket++) { + int count = 0; + for (PageInfo* pg = (PageInfo*)heap_pages[bucket].head; pg; pg = pg->next) + count++; + if (count < 2) + continue; + + CompactPageStat* stats = (CompactPageStat*)malloc (count * sizeof(CompactPageStat)); + size_t total_free = 0; + int n = 0; + for (PageInfo* pg = (PageInfo*)heap_pages[bucket].head; pg; pg = pg->next) { + stats[n].page = pg; + stats[n].live = pg->num_cells - pg->num_free_cells; + n++; + total_free += pg->num_free_cells; + } + qsort (stats, n, sizeof(CompactPageStat), compact_stat_cmp); + + // choose the COMPLETE source set first, sparse-first: a page + // accepted as a source leaves the destination pool, and the + // remaining pool must hold every already-accepted live cell + // plus this page's. (Selecting and evacuating in one pass let + // an early DESTINATION later be picked as a source via its + // stale live count — evacuating more cells than the accounting + // reserved space for.) + size_t dest_free = total_free; + size_t src_live = 0; + EJSList src_pages; + memset (&src_pages, 0, sizeof(src_pages)); + for (int i = 0; i < n; i++) { + PageInfo* pg = stats[i].page; + size_t live = (size_t)stats[i].live; + if (live == 0) + continue; // the sweep freelists empties; belt only + if (dest_free - pg->num_free_cells < src_live + live) + break; // the sparsest candidate doesn't fit; denser ones won't either + if (compact_page_has_pins(pg)) + continue; // pinned cells sweep in place; the page stays a destination + _ejs_list_detach_node (&heap_pages[bucket], (EJSListNode*)pg); + _ejs_list_append_node (&src_pages, (EJSListNode*)pg); + dest_free -= pg->num_free_cells; + src_live += live; + } + + // sources are off the bucket list now: every listed page is a + // pure destination, so the cursor can walk it freely + PageInfo* cursor = NULL; + PageInfo* src; + while ((src = (PageInfo*)src_pages.head) != NULL) { + _ejs_list_detach_node (&src_pages, (EJSListNode*)src); + compact_evacuate_page (bucket, src, &cursor); + _ejs_list_append_node (&evac_pages, (EJSListNode*)src); + } + free (stats); + } + + // 2. fixup: rewrite every reference that can name a moved cell, and + // clear the cycle's pins while walking the live set. Runs even + // when nothing was evacuated — the pins must reset either way. + root_registry_foreach (compact_fixup_slot); + for (int i = 0; i < _ejs_num_modules; i++) { + EJSObject* mod = (EJSObject*)_ejs_modules[i]; + if (mod->ops) + OP(mod,Scan)(mod, compact_fixup_slot); + } + // gc-frame slots' referents were all conservatively pinned (full GC + // never skips frame records), so these rewrites are no-ops today; + // walked anyway so precision changes can't silently break this pass + walk_gc_frames(compact_fixup_slot); + for (int i = 0; i < _ejs_heap.remset_count; i++) { + GCObjectPtr o = (GCObjectPtr)_ejs_heap.remset[i]; + if (_ejs_gc_is_forwarded(o)) + _ejs_heap.remset[i] = _ejs_gc_forwarding_addr(o); + } + old_gen_walk (compact_fixup_object); // old pages (sources skip via FORWARDED) + LOS + for (PageInfo* pg = (PageInfo*)heap_priv.young_pages.head; pg; pg = pg->next) { + GCObjectPtr p = pg->page_start; + for (int c = 0; c < CELLS_IN_PAGE(pg); c++, p += pg->cell_size) + if (!cell_is_free(pg->page_bitmap[c])) + compact_fixup_object(p); + } + + // 3. release the sources: nothing reads the forwarding records + // anymore; the pages go back to their arenas. No finalizers run — + // the objects live on at their new addresses. + PageInfo* pg; + while ((pg = (PageInfo*)evac_pages.head) != NULL) { + _ejs_list_detach_node (&evac_pages, (EJSListNode*)pg); + memset (pg->page_start, 0xa7, PAGE_SIZE); // 0xa7: FORWARDED must stay clear in poison + memset (pg->page_bitmap, CELL_FREE, pg->num_cells * sizeof(BitmapCell)); + pg->num_free_cells = pg->num_cells; + pg->bump_ptr = pg->page_start; + Arena* arena = (Arena*)PTR_TO_ARENA(pg->page_start); + EJS_LIST_PREPEND (pg, arena->free_pages); + compact_freed_pages++; + } + + if (gc_profile) + _ejs_log ("EJS_GC_PROFILE: compact: moved=%llu freed-pages=%llu\n", + (unsigned long long)(compact_moved_objs - moved_before), + (unsigned long long)(compact_freed_pages - freed_before)); +} +// ============== end mostly-copying major compaction ====================== + +void +_ejs_gc_collect_inner(EJSBool shutting_down) +{ +#if gc_timings > 1 + struct timeval tvbefore, tvafter; +#endif + + // very simple stop the world collector + SPEW(1, _ejs_log ("collection started\n")); + + num_roots = 0; + white_objs = 0; + large_objs = 0; + total_objs = 0; + + // full collections need young pages in bitmap-authoritative + // form (active bump pages have no valid FREE bits or counts) + young_normalize_for_full_gc(); + +#if gc_timings > 1 + gettimeofday (&tvbefore, NULL); +#endif + + struct timeval prof_tv_begin, prof_tv_end; + if (gc_profile) + gettimeofday (&prof_tv_begin, NULL); + + struct timeval fg[8]; + if (!shutting_down) { + gettimeofday (&fg[0], NULL); + mark_from_roots(); + + total_objs = num_roots; + + mark_from_modules(); + gettimeofday (&fg[1], NULL); + + mark_thread_stack(); + + mark_generator_stacks(); + gettimeofday (&fg[2], NULL); + + // dirty objects await their deferred minor scan and may + // hold the only reference to young data — root them + for (int i = 0; i < _ejs_heap.remset_count; i++) + mark_object_root((GCObjectPtr)_ejs_heap.remset[i]); + gettimeofday (&fg[3], NULL); + + process_worklist(); + gettimeofday (&fg[4], NULL); + + // survival + pin census must walk the heap BEFORE the + // sweep frees the white cells + if (gc_profile) + profile_pre_sweep(); + gettimeofday (&fg[5], NULL); + if (gc_profile) { +#define FGUS(a,b) ((long long)(((b).tv_sec - (a).tv_sec) * 1000000LL + ((b).tv_usec - (a).tv_usec))) + _ejs_log ("EJS_GC_PROFILE: full-gc phases: roots+modules=%lldus stacks=%lldus remset-roots=%lldus (remset=%d) worklist=%lldus census=%lldus\n", + FGUS(fg[0],fg[1]), FGUS(fg[1],fg[2]), FGUS(fg[2],fg[3]), + _ejs_heap.remset_count, FGUS(fg[3],fg[4]), FGUS(fg[4],fg[5])); +#undef FGUS + } + } + +#if gc_timings > 1 + gettimeofday (&tvafter, NULL); +#endif + +#if gc_timings > 1 + { + uint64_t usec_before = tvbefore.tv_sec * 1000000 + tvbefore.tv_usec; + uint64_t usec_after = tvafter.tv_sec * 1000000 + tvafter.tv_usec; + + _ejs_log ("gc scan took %gms\n", (usec_after - usec_before) / 1000.0); + } +#endif + +#if gc_timings > 1 + gettimeofday (&tvbefore, NULL); +#endif + + sweep_heap(); + + // mostly-copying: evacuate the sparse pages' unpinned live + // cells, rewrite every reference, return emptied pages to their + // arenas. (Skipped on the shutdown collection — nothing left to + // move for.) + if (compact_enabled && !shutting_down) + compact_old_gen(); + + // the remembered state may dangle into cells this sweep just + // freed — rebuild it from the live old gen + if (!shutting_down) + remset_rebuild_after_full_gc(); + + if (gc_profile && !shutting_down) { + gettimeofday (&prof_tv_end, NULL); + uint64_t usec = (prof_tv_end.tv_sec - prof_tv_begin.tv_sec) * 1000000ULL + + (prof_tv_end.tv_usec - prof_tv_begin.tv_usec); + profile_report_cycle_end (usec); + } + +#if gc_timings > 1 + { + gettimeofday (&tvafter, NULL); + } +#endif + +#if gc_timings > 1 + { + uint64_t usec_before = tvbefore.tv_sec * 1000000 + tvbefore.tv_usec; + uint64_t usec_after = tvafter.tv_sec * 1000000 + tvafter.tv_usec; + + _ejs_log ("gc sweep took %gms\n", (usec_after - usec_before) / 1000.0); + } +#endif + +#if gc_timings > 1 + _ejs_log ("_ejs_gc_collect stats:\n"); + _ejs_log (" num_roots: %d\n", num_roots); + _ejs_log (" total objects: %d\n", total_objs); + _ejs_log (" num large objects: %d\n", large_objs); + _ejs_log (" garbage objects: %d\n", white_objs); +#endif + + // age the survivors: this epoch's black is next epoch's white + mark_epoch_advance(); + + if (shutting_down) { + root_registry_shutdown(); + + SPEW(1, _ejs_log ("final gc page statistics:\n"); + for (int hp = 0; hp < HEAP_PAGELISTS_COUNT; hp++) { + int len = 0; + + EJS_LIST_FOREACH (&heap_pages[hp], PageInfo, page, { + len ++; + }); + + _ejs_log (" size: %d pages: %d\n", 1<<(hp + 3), len); + }); + } +#if sanity + else { + for (int hp = 0; hp < HEAP_PAGELISTS_COUNT; hp++) { + EJS_LIST_FOREACH (&heap_pages[hp], PageInfo, page, { + for (int c = 0; c < CELLS_IN_PAGE (page); c ++) { + if (!cell_is_free(page->page_bitmap[c]) && !cell_is_white(page->page_bitmap[c])) + continue; + } + }) + } + } +#endif + SPEW(1, _ejs_log ("collection finished\n")); +} + +// heap footprint measured after the last collection's sweep. The +// collection trigger scales with this: a fixed allocation budget on a +// growing live set makes total GC work quadratic in heap size (shapes +// shapes moved per-object property storage into the GC heap, which pushed +// stage2's self-compile off that cliff — hours of back-to-back full +// marks of a ~900MB heap). Letting the heap grow ~gc_growth_pct% +// between full collections keeps total mark work linear (see +// full_gc_trigger; compaction shrinks this after a drop in live set, +// so the cadence adapts back down too). +size_t heap_size_at_last_gc = 0; + +void +_ejs_gc_collect(const char *reason) +{ + SPEW(1, _ejs_log ("_ejs_gc_collect(%s)\n", reason)); + prof_gc_reason = reason; +#if gc_timings > 0 + struct timeval tvbefore, tvafter; + + gettimeofday (&tvbefore, NULL); + + int heap_size = calc_heap_size(); +#endif + + _ejs_gc_collect_inner(EJS_FALSE); + + // post-sweep footprint drives the proportional collection trigger + // (see heap_size_at_last_gc) + heap_size_at_last_gc = calc_heap_size(); + +#if gc_timings > 0 + gettimeofday (&tvafter, NULL); + + uint64_t usec_before = tvbefore.tv_sec * 1000000 + tvbefore.tv_usec; + uint64_t usec_after = tvafter.tv_sec * 1000000 + tvafter.tv_usec; + + _ejs_log ("gc collect took %gms\n", (usec_after - usec_before) / 1000.0); + _ejs_log (" for a heap size of %zdMB\n", heap_size/(1024*1024)); +#if gc_timings > 1 + _ejs_gc_dump_heap_stats(); +#endif +#endif +} diff --git a/runtime/ejs-gc-mark.c b/runtime/ejs-gc-mark.c new file mode 100644 index 00000000..880ef1a9 --- /dev/null +++ b/runtime/ejs-gc-mark.c @@ -0,0 +1,622 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=cpp: + */ + +// marking: the tri-color worklist, the precise slot scanners, the +// conservative stack/register/generator-stack scanners with the +// gc-frame skip machinery, and the full-GC mark phases. + +#include "ejs-gc-internal.h" + +#define MAX_WORKLIST_SEGMENT_SIZE 512 +typedef struct _WorkListSegmnt { + EJS_SLIST_HEADER(struct _WorkListSegmnt); + int size; + GCObjectPtr work_list[MAX_WORKLIST_SEGMENT_SIZE]; +} WorkListSegment; + +typedef struct { + WorkListSegment *list; + WorkListSegment *free_list; +} WorkList; + +static WorkList work_list; + +void +_ejs_gc_worklist_init() +{ + work_list.list = NULL; + work_list.free_list = NULL; +} + +static void +_ejs_gc_worklist_push(GCObjectPtr obj) +{ + if (obj == NULL) + return; + + WorkListSegment *segment; + + if (EJS_UNLIKELY(!work_list.list || work_list.list->size == MAX_WORKLIST_SEGMENT_SIZE)) { + // we need a new segment + if (work_list.free_list) { + // take one from the free list + segment = work_list.free_list; + EJS_SLIST_DETACH_HEAD(segment, work_list.free_list); + } + else { + segment = (WorkListSegment*)malloc (sizeof(WorkListSegment)); + segment->size = 0; + } + EJS_SLIST_ATTACH(segment, work_list.list); + } + else { + segment = work_list.list; + } + + segment->work_list[segment->size++] = obj; +} + +static GCObjectPtr +_ejs_gc_worklist_pop() +{ + if (work_list.list == NULL || work_list.list->size == 0/* shouldn't happen, since we push the page to the free list if we hit 0 */) + return NULL; + + WorkListSegment *segment = work_list.list; + + GCObjectPtr rv = segment->work_list[--segment->size]; + if (segment->size == 0) { + EJS_SLIST_DETACH_HEAD(segment, work_list.list); + EJS_SLIST_ATTACH(segment, work_list.free_list); + } + return rv; +} + +#define WORKLIST_PUSH_AND_GRAY(x) EJS_MACRO_START \ + if (is_white((GCObjectPtr)x)) { \ + _ejs_gc_worklist_push((GCObjectPtr)(x)); \ + set_gray ((GCObjectPtr)(x)); \ + } \ + EJS_MACRO_END + +#define WORKLIST_PUSH_AND_GRAY_CELL(x, cell) EJS_MACRO_START \ + if (cell_is_white(cell)) { \ + _ejs_gc_worklist_push((GCObjectPtr)(x)); \ + cell_set_gray(&cell); \ + } \ + EJS_MACRO_END + +static void +set_gray (GCObjectPtr ptr) +{ + uint32_t cell_idx; + PageInfo *page = find_page_and_cell(ptr, &cell_idx); + if (!page) + return; + + cell_set_gray(&page->page_bitmap[cell_idx]); +} + +static void +set_black (GCObjectPtr ptr) +{ + uint32_t cell_idx; + PageInfo *page = find_page_and_cell(ptr, &cell_idx); + if (!page) + return; + + cell_set_black(&page->page_bitmap[cell_idx]); +} + +static EJSBool +is_white (GCObjectPtr ptr) +{ + uint32_t cell_idx; + PageInfo *page = find_page_and_cell(ptr, &cell_idx); + if (!page) + return EJS_FALSE; + + return cell_is_white(page->page_bitmap[cell_idx]); +} + +// the mark-path scan callback. Slot-based per the new +// EJSValueFunc contract — this non-moving path only reads through the +// slot; the mover's evacuation callback is what rewrites it. +static void +_scan_ejsvalue (ejsval* slot) +{ + ejsval val = *slot; + if (!EJSVAL_IS_TRACEABLE_IMPL(val)) return; + + GCObjectPtr gcptr = (GCObjectPtr)EJSVAL_TO_GCTHING_IMPL(val); + + if (gcptr == NULL) return; + + WORKLIST_PUSH_AND_GRAY(gcptr); +} + +static void +_scan_from_ejsobject(EJSObject* obj) +{ + // freshly allocated objects are zeroed but not yet initialized (their + // constructor may trigger a collection before _ejs_init_object runs); + // there's nothing to scan in them yet. + if (obj->ops == NULL) + return; + OP(obj,Scan)(obj, _scan_ejsvalue); +} + +static void +_scan_from_ejsprimstr(EJSPrimString *primStr) +{ + EJSPrimStringType strtype = EJS_PRIMSTR_GET_TYPE(primStr); + + switch (strtype) { + case EJS_STRING_ROPE: + // inline _scan_ejsvalue's push logic here to save creating an ejsval from the primStr only to destruct + // it in _scan_ejsvalue + + WORKLIST_PUSH_AND_GRAY(primStr->data.rope.left); + WORKLIST_PUSH_AND_GRAY(primStr->data.rope.right); + break; + case EJS_STRING_DEPENDENT: + WORKLIST_PUSH_AND_GRAY(primStr->data.dependent.dep); + break; + case EJS_STRING_FLAT: + // nothing to do here + break; + } +} + +static void +_scan_from_ejsprimsym(EJSPrimSymbol *primSymbol) +{ + _scan_ejsvalue (&primSymbol->description); +} + +static void +_scan_from_ejsclosureenv(EJSClosureEnv *env) +{ + for (uint32_t i = 0; i < env->length; i ++) { + _scan_ejsvalue (&env->slots[i]); + } +} + +GCObjectPtr *stack_bottom; + +void +_ejs_gc_mark_thread_stack_bottom(GCObjectPtr* btm) +{ + stack_bottom = btm; + // the write barrier's transient-slot upper bound starts at + // the main stack's bottom (generator push/pop moves it) + _ejs_heap.current_stack_end = (void*)btm; +} + +static void +mark_pointers_in_range(GCObjectPtr* low, GCObjectPtr* high) +{ + GCObjectPtr* p; + for (p = low; p < high-1; p++) { + GCObjectPtr gcptr; + +#if OSX + // really a 64 bit check here, since for 64 bit systems, ejsvals can be stuck in registers, so we need to check if it's a valid + // ejsval gcthing as well. + ejsval ep = *(ejsval*)p; + if (EJSVAL_IS_GCTHING_IMPL(ep)) + gcptr = (GCObjectPtr)EJSVAL_TO_GCTHING_IMPL(ep); + else +#endif + gcptr = *p; + + if (gcptr == NULL) continue; // skip nulls. + if ((char*)gcptr < conservative_lo || (char*)gcptr >= conservative_hi) + continue; // cheap prefilter: outside every arena/LOS block + + uint32_t cell_idx; + + PageInfo *page = find_page_and_cell(gcptr, &cell_idx); + if (!page) continue; // skip values outside our heap. + + // XXX more checks before we start treating the pointer like a GCObjectPtr? + BitmapCell cell = page->page_bitmap[cell_idx]; + if (!cell_is_allocated(page, cell_idx, cell)) continue; + + // during a minor collection conservative hits PIN young + // cells in place; nothing else is this collection's business + if (in_minor_gc) { minor_conservative_hit(page, cell_idx); continue; } + + // a conservative hit PINS: the compacting major must sweep this + // cell in place. Recorded even when the target is already + // marked (the white check below is a marking optimization, not + // a pin filter). profile_note_pin sets the same bit plus stats. + if (gc_profile) profile_note_pin(page, cell_idx, gcptr); + else *(GCObjectHeader*)(page->page_start + ((size_t)cell_idx * page->cell_size)) |= EJS_GC_HEADER_PINNED; + + if (!cell_is_white(cell)) continue; // skip pointers to gray/black cells + + // canonicalize interior pointers to the start of their cell; the + // worklist processing reads the object header from the pointer. + gcptr = page->page_start + (cell_idx * page->cell_size); + + WORKLIST_PUSH_AND_GRAY_CELL(gcptr, page->page_bitmap[cell_idx]); + } +} + +// gc-frame slots are stack memory, so the conservative +// stack scan would see every precisely-rooted value a second time and +// pin it through its own slot — precision would never move anything. +// During a minor, the scan skips the frame records of the stack being +// scanned (their slots are walked precisely and rewritten). Full GC +// never skips: it relies on the conservative scan seeing the slots. +typedef struct { char* lo; char* hi; } FrameSkipRange; +#define MAX_FRAME_SKIP 1024 +static FrameSkipRange frame_skip[MAX_FRAME_SKIP]; +static int frame_skip_count; + +void +set_frame_skip_chain(void* chain_head) +{ + frame_skip_count = 0; + for (EJSGCFrame* f = (EJSGCFrame*)chain_head; f; f = f->prev) { + if (frame_skip_count == MAX_FRAME_SKIP) break; // partial skip = extra pins only + char* lo = (char*)f; + char* hi = lo + 16 + 8 * f->count; + // insertion sort by lo; chains are short and near-sorted + int i = frame_skip_count++; + while (i > 0 && frame_skip[i - 1].lo > lo) { + frame_skip[i] = frame_skip[i - 1]; + i--; + } + frame_skip[i].lo = lo; + frame_skip[i].hi = hi; + } +} + +void +clear_frame_skip(void) +{ + frame_skip_count = 0; +} + +static void +mark_ejsvals_in_range(void* low, void* high) +{ + // per-call skip cursor: ranges below `low` are behind us + int fr = 0; + while (fr < frame_skip_count && frame_skip[fr].hi <= (char*)low) fr++; + void* p = low; +#if IOS + while (((uintptr_t)p) & 0x7) { + p++; + } +#endif + for (; p < high - sizeof(ejsval); p += sizeof(ejsval)) { + // inside a gc-frame record? its slots are precise roots + while (fr < frame_skip_count && frame_skip[fr].hi <= (char*)p) fr++; + if (fr < frame_skip_count && (char*)p >= frame_skip[fr].lo) continue; + ejsval candidate_val = *((ejsval*)p); + GCObjectPtr gcptr; + if (EJSVAL_IS_GCTHING_IMPL(candidate_val)) { + gcptr = (GCObjectPtr)EJSVAL_TO_GCTHING_IMPL(candidate_val); + } + else { + // also treat the slot as a raw, untagged pointer: optimized + // (opt -O2) code compiled by ejs unboxes closure envs and + // objects once and keeps/spills the raw pointer, with the + // tagged ejsval potentially dead. + gcptr = *(GCObjectPtr*)p; + } + + if (gcptr == NULL) continue; // skip nulls. + if ((char*)gcptr < conservative_lo || (char*)gcptr >= conservative_hi) + continue; // cheap prefilter: outside every arena/LOS block + + uint32_t cell_idx; + PageInfo *page = find_page_and_cell(gcptr, &cell_idx); + if (page) { + // XXX more checks before we start treating the pointer like a GCObjectPtr? + BitmapCell cell = page->page_bitmap[cell_idx]; + if (!cell_is_allocated(page, cell_idx, cell)) continue; + + // minor collections only pin young cells here + if (in_minor_gc) { minor_conservative_hit(page, cell_idx); continue; } + + // a conservative hit PINS: the compacting major must sweep + // this cell in place (recorded even when already marked) + if (gc_profile) profile_note_pin(page, cell_idx, gcptr); + else *(GCObjectHeader*)(page->page_start + ((size_t)cell_idx * page->cell_size)) |= EJS_GC_HEADER_PINNED; + + if (!cell_is_white(cell)) continue; // skip pointers to gray/black cells + + // canonicalize interior pointers to the start of their cell; the + // worklist processing reads the object header from the pointer. + gcptr = page->page_start + (cell_idx * page->cell_size); + + WORKLIST_PUSH_AND_GRAY_CELL(gcptr, page->page_bitmap[cell_idx]); + } + } +} + +#define MAX_GENERATORS 256 +static int generator_count = 0; +static EJSGenerator* generators[MAX_GENERATORS]; + +// walk every gc-frame chain — the running stack's (the +// seam head) plus every suspended generator's saved chain and every +// ACTIVE generator's parked caller segment. Chains are per-stack and +// disjoint; records live in stack frames that stay mapped for exactly +// as long as they are linked (returns unlink, catches re-link their +// own frame past unwound callees, the generator hooks swap heads at +// every stack switch). +void +walk_gc_frames(void (*slot_fn)(ejsval*)) +{ + for (EJSGCFrame* f = (EJSGCFrame*)_ejs_heap.gc_frame_head; f; f = f->prev) + for (uintptr_t i = 0; i < f->count; i++) + slot_fn(&f->slots[i]); + for (EJSGenerator* g = _ejs_generator_registry; g; g = g->reg_next) + for (EJSGCFrame* f = (EJSGCFrame*)g->gc_frame_head; f; f = f->prev) + for (uintptr_t i = 0; i < f->count; i++) + slot_fn(&f->slots[i]); + for (int gi = 0; gi < generator_count; gi++) + for (EJSGCFrame* f = (EJSGCFrame*)generators[gi]->caller_gc_frame_head; f; f = f->prev) + for (uintptr_t i = 0; i < f->count; i++) + slot_fn(&f->slots[i]); +} + +static void +mark_root_slot(ejsval* root) +{ + num_roots++; + ejsval rootval = *root; + if (!EJSVAL_IS_GCTHING_IMPL(rootval)) + return; + GCObjectPtr root_ptr = (GCObjectPtr)EJSVAL_TO_GCTHING_IMPL(rootval); + if (root_ptr == NULL) + return; + uint32_t cell_idx; + PageInfo* page = find_page_and_cell(root_ptr, &cell_idx); + if (!page) + return; + + BitmapCell cell = page->page_bitmap[cell_idx]; + if (cell_is_free(cell)) return; // skip free cells + if (!cell_is_white(cell)) return; // skip pointers to gray/black cells + WORKLIST_PUSH_AND_GRAY_CELL(root_ptr, page->page_bitmap[cell_idx]); +} + +void +mark_from_roots() +{ + SPEW (2, _ejs_log ("marking from roots")); + root_registry_foreach (mark_root_slot); + SPEW (2, _ejs_log ("done marking from roots")); +} + +void +mark_from_modules() +{ + SPEW(2, _ejs_log ("marking from module exotics")); + + for (int i = 0; i < _ejs_num_modules; i ++) { + EJSObject* mod = (EJSObject*)_ejs_modules[i]; + // modules are static globals whose object headers aren't set up + // until _ejs_require_init; if a collection happens before that + // (e.g. EJS_GC_EVERY_N_ALLOC during _ejs_init) there's nothing to + // scan yet. + if (mod->ops == NULL) + continue; + _scan_from_ejsobject(mod); + } +} + +#if TARGET_CPU_ARM +#define MARK_REGISTERS EJS_MACRO_START \ + GCObjectPtr __r0, __r1, __r2, __r3, __r4, __r5, __r6, __r7, __r8, __r9, __r10, __r11, __r12, __end; \ + __asm ("str r0, %0; str r1, %1; str r2, %2; str r3, %3; str r4, %4; str r5, %5; str r6, %6;" \ + "str r7, %7; str r8, %8; str r9, %9; str r10, %10; str r11, %11; str r12, %12;" \ + : "=m"(__r0), "=m"(__r1), "=m"(__r2), "=m"(__r3), "=m"(__r4), \ + "=m"(__r5), "=m"(__r6), "=m"(__r7), "=m"(__r8), "=m"(__r9), \ + "=m"(__r10), "=m"(__r11), "=m"(__r12)); \ + \ + mark_pointers_in_range(&__end, &__r0); \ + EJS_MACRO_END +#elif TARGET_CPU_ARM64 +// spill the callee-saved registers (x19-x28, plus fp) and treat them as +// roots. code compiled by ejs (opt -O2) keeps live ejsvals in callee-saved +// registers across calls, and the mostly -O0 runtime doesn't reliably save +// all of them anywhere the stack scan would see. (an empty MARK_REGISTERS +// here let live objects be collected and their cells reused -> heap +// corruption.) +#define MARK_REGISTERS EJS_MACRO_START \ + GCObjectPtr __regs[21]; \ + __asm volatile ("stp x19, x20, [%0, #0]\n\t" \ + "stp x21, x22, [%0, #16]\n\t" \ + "stp x23, x24, [%0, #32]\n\t" \ + "stp x25, x26, [%0, #48]\n\t" \ + "stp x27, x28, [%0, #64]\n\t" \ + "str x29, [%0, #80]\n\t" \ + /* llvm will spill gprs into the callee-saved simd \ + registers under pressure, so scan those too */ \ + "stp d8, d9, [%0, #88]\n\t" \ + "stp d10, d11, [%0, #104]\n\t" \ + "stp d12, d13, [%0, #120]\n\t" \ + "stp d14, d15, [%0, #136]" \ + : : "r"(__regs) : "memory"); \ + __regs[19] = __regs[20] = NULL; \ + /* mark_pointers_in_range scans [low, high-1) */ \ + mark_pointers_in_range(__regs, __regs + 21); \ + EJS_MACRO_END +#elif TARGET_CPU_AMD64 +#define MARK_REGISTERS EJS_MACRO_START \ + GCObjectPtr __rax, __rbx, __rcx, __rdx, __rsi, __rdi, __rbp, __rsp, __r8, __r9, __r10, __r11, __r12, __r13, __r14, __r15, __end; \ + __asm ("movq %%rax, %0; movq %%rbx, %1; movq %%rcx, %2; movq %%rdx, %3; movq %%rsi, %4;" \ + "movq %%rdi, %5; movq %%rbp, %6; movq %%rsp, %7; movq %%r8, %8; movq %%r9, %9;" \ + "movq %%r10, %10; movq %%r11, %11; movq %%r12, %12; movq %%r13, %13; movq %%r14, %14; movq %%r15, %15;" \ + : "=m"(__rax), "=m"(__rbx), "=m"(__rcx), "=m"(__rdx), "=m"(__rsi), \ + "=m"(__rdi), "=m"(__rbp), "=m"(__rsp), "=m"(__r8), "=m"(__r9), \ + "=m"(__r10), "=m"(__r11), "=m"(__r12), "=m"(__r13), "=m"(__r14), "=m"(__r15)); \ + \ + mark_pointers_in_range(&__end, &__rax); \ + EJS_MACRO_END +#elif TARGET_CPU_X86 +#define MARK_REGISTERS // just keep the build limping along +#else +#error "put code here to mark registers" +#endif + +// (MAX_GENERATORS / generators[] / generator_count moved above +// walk_gc_frames, which walks the active chain's parked caller +// segments) + +void +_ejs_gc_push_generator(EJSGenerator* gen) +{ + if (generator_count >= MAX_GENERATORS) { + _ejs_log ("too many nested generators (max %d)\n", MAX_GENERATORS); + abort(); + } + generators[generator_count++] = gen; + // keep the barrier's transient-slot bound on the CURRENT stack + _ejs_heap.current_stack_end = gen->stack + gen->stack_size; + // swap in this stack's gc-frame chain; the caller's segment + // parks on the generator until the matching pop + gen->caller_gc_frame_head = _ejs_heap.gc_frame_head; + _ejs_heap.gc_frame_head = gen->gc_frame_head; + gen->gc_frame_head = NULL; // the live chain is the seam head now +} + +void +_ejs_gc_pop_generator() +{ + generator_count--; + EJSGenerator* gen = generators[generator_count]; + _ejs_heap.current_stack_end = generator_count > 0 + ? generators[generator_count - 1]->stack + generators[generator_count - 1]->stack_size + : (void*)stack_bottom; + // park this stack's chain on the generator (walked while + // suspended), restore the caller's segment + gen->gc_frame_head = _ejs_heap.gc_frame_head; + _ejs_heap.gc_frame_head = gen->caller_gc_frame_head; + gen->caller_gc_frame_head = NULL; +} + +void +mark_thread_stack() +{ + prof_pin_source = PROF_SRC_REGS; + MARK_REGISTERS; + prof_pin_source = PROF_SRC_CSTACK; + + GCObjectPtr stack_top = NULL; + + // The CURRENT machine stack. When the mutator is running on a + // generator's malloc'd stack (collections happen inside + // _ejs_gc_alloc, which generator bodies call), [&stack_top, + // stack_bottom) is NOT a stack range — it spans from the malloc heap + // to the main stack across unmapped memory. Scan only up to the + // running generator's stack end; mark_generator_stacks covers the + // suspended caller segments. + void* high = (void*)stack_bottom; + if (generator_count > 0) { + EJSGenerator* running = generators[generator_count - 1]; + high = running->stack + running->stack_size; + } + + mark_ejsvals_in_range(((void*)&stack_top) + sizeof(GCObjectPtr), high); +} + +// mark a known heap object as a root (page cell or LOS both resolve +// through find_page_and_cell; the pointer must be an object base) +void +mark_object_root(GCObjectPtr ptr) +{ + uint32_t cell_idx; + PageInfo* page = find_page_and_cell(ptr, &cell_idx); + if (!page) + return; + BitmapCell cell = page->page_bitmap[cell_idx]; + if (!cell_is_allocated(page, cell_idx, cell)) + return; + if (in_minor_gc) { + // minor collections: a young root pins; an old root's slots may hold + // young references, so queue it for the precise minor scan + // (duplicates are harmless — evacuation is idempotent) + if (page->young) minor_conservative_hit(page, cell_idx); + else minor_wl_push(ptr); + return; + } + if (!cell_is_white(cell)) + return; + WORKLIST_PUSH_AND_GRAY_CELL(ptr, page->page_bitmap[cell_idx]); +} + +// The chain of ACTIVE generators (generators whose bodies are on the +// current stack chain; push on start/resume, pop on yield/completion — +// generators[generator_count-1] owns the stack we are executing on). +// mark_thread_stack scans the running stack; this covers the rest: +// +// - each active generator OBJECT is a root for the cycle (its specop +// scan conservatively marks its own suspended frames and both saved +// ucontexts, i.e. the register files); +// - the SUSPENDED CALLER segment behind each swap-in: frames from the +// caller_stack_top recorded at the resume site up to that caller's +// stack end — the main stack (stack_bottom) for the outermost +// generator, the parent generator's stack end for nested ones. +// +// Suspended generators NOT in the chain need nothing here: if their +// object is reachable its scan covers their stack; if it is not, nothing +// on that stack is reachable either. +void +mark_generator_stacks() +{ + prof_pin_source = PROF_SRC_CSTACK; // the suspended segments ARE C stack + for (int i = 0; i < generator_count; i++) { + EJSGenerator* gen = generators[i]; + + mark_object_root((GCObjectPtr)gen); + + void* seg_high = (i == 0) ? (void*)stack_bottom + : generators[i - 1]->stack + generators[i - 1]->stack_size; + if (gen->caller_stack_top) { + // this caller segment's frames are the chain parked + // at push time (minor only; a full GC leaves skips empty) + if (in_minor_gc) set_frame_skip_chain(gen->caller_gc_frame_head); + mark_ejsvals_in_range(gen->caller_stack_top, seg_high); + if (in_minor_gc) clear_frame_skip(); + } + } +} + +void +process_worklist() +{ + GCObjectPtr p; + while ((p = _ejs_gc_worklist_pop())) { + set_black (p); + GCObjectHeader* headerp = (GCObjectHeader*)p; + if ((*headerp & EJS_SCAN_TYPE_OBJECT) != 0) + _scan_from_ejsobject((EJSObject*)p); + else if ((*headerp & EJS_SCAN_TYPE_PRIMSTR) != 0) + _scan_from_ejsprimstr((EJSPrimString*)p); + else if ((*headerp & EJS_SCAN_TYPE_PRIMSYM) != 0) + _scan_from_ejsprimsym((EJSPrimSymbol*)p); + else if ((*headerp & EJS_SCAN_TYPE_CLOSUREENV) != 0) + _scan_from_ejsclosureenv((EJSClosureEnv*)p); + } + + EJS_ASSERT(work_list.list == NULL); +} + +void +_ejs_gc_mark_conservative_range(void* low, void* high) { + // only the generator scan uses this entry point (suspended stacks + + // saved ucontexts) — attribute its pins accordingly + int prev_src = prof_pin_source; + prof_pin_source = PROF_SRC_GENSTACK; + mark_ejsvals_in_range(low, high); + prof_pin_source = prev_src; +} diff --git a/runtime/ejs-gc-minor.c b/runtime/ejs-gc-minor.c new file mode 100644 index 00000000..7c296006 --- /dev/null +++ b/runtime/ejs-gc-minor.c @@ -0,0 +1,695 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=cpp: + */ + +// the generational nursery: young pages over the seam cursors, the +// mostly-copying minor collection (pin, evacuate, forward, rewrite), +// and the remembered-set discipline. + +#include "ejs-gc-internal.h" + +EJSHeapContext _ejs_heap; // exported: the per-isolate context (the emitter seam) + +EJSBool nursery_enabled; // EJS_GC_NURSERY=off selects the old collector +EJSBool in_minor_gc; // the shared mark helpers dispatch on this + +EJSHeapPriv heap_priv; // the private half of the (single) isolate's context + +#define NURSERY_REMSET_CAPACITY (64 * 1024) + +// EJS_GC_MINOR_SPEW=1: per-event tracing for nursery debugging +static EJSBool minor_spew; +#define MINOR_SPEW(...) EJS_MACRO_START if (minor_spew) _ejs_log (__VA_ARGS__); EJS_MACRO_END + +// the seam cursors are authoritative while a page is being bumped; fold +// them back into the page before any collection looks at bump_ptr +static void +young_flush_bumps(void) +{ + for (int i = 0; i < EJS_GC_NUM_SIZE_CLASSES; i++) { + if (heap_priv.young_current[i]) + heap_priv.young_current[i]->bump_ptr = _ejs_heap.bump[i]; + } +} + +static void +young_page_retire_current(int idx) +{ + PageInfo* page = heap_priv.young_current[idx]; + if (!page) return; + page->bump_ptr = _ejs_heap.bump[idx]; + _ejs_list_append_node (&heap_priv.young_pages, (EJSListNode*)page); + heap_priv.young_current[idx] = NULL; + _ejs_heap.bump[idx] = _ejs_heap.limit[idx] = NULL; +} + +// grab a fresh page from the nursery arena for class idx, or NULL when +// the nursery is exhausted (the caller runs a minor collection) +static PageInfo* +young_page_install(int idx, size_t cell_size) +{ + Arena* arena = heap_priv.nursery_arena; + PageInfo* info = NULL; + if (in_minor_gc) { + _ejs_log ("GC BUG: young_page_install during a minor collection\n"); + abort(); + } + if (arena->free_pages) { + info = arena->free_pages; + EJS_LIST_DETACH(info, arena->free_pages); + info->cell_size = cell_size; + info->num_cells = CELLS_OF_SIZE(cell_size); + info->num_free_cells = info->num_cells; + } else { + info = alloc_page_from_arena(arena, cell_size); + if (!info) return NULL; + } + info->young = 1; + info->bump_ptr = info->page_start; + heap_priv.young_alloced += PAGE_SIZE; + // colors start at the CURRENT white (a young cell must never read + // as black mid-cycle); allocated-ness comes from the bump rule + memset (info->page_bitmap, cell_white_color(), info->num_cells * sizeof(BitmapCell)); + heap_priv.young_current[idx] = info; + _ejs_heap.bump[idx] = info->page_start; + _ejs_heap.limit[idx] = info->page_end; + return info; +} + +// an emptied young page leaves heap_priv.young_pages for the nursery +// arena's free list (called from _ejs_finalize_obj when a full sweep +// kills a survivor page's last cell) +void +young_page_freed(PageInfo* info, Arena* arena) +{ + EJS_ASSERT(arena && arena->is_nursery); + _ejs_list_detach_node (&heap_priv.young_pages, (EJSListNode*)info); + info->young = 0; + info->bump_ptr = info->page_start; + EJS_LIST_PREPEND (info, arena->free_pages); +} + +// set when a scan leaves a still-young (pinned) referent behind — the +// dirty owner carries to the next cycle +EJSBool minor_scan_saw_young; + +void +minor_wl_push(GCObjectPtr p) +{ + if (heap_priv.wl_count == heap_priv.wl_cap) { + heap_priv.wl_cap = heap_priv.wl_cap ? heap_priv.wl_cap * 2 : 4096; + heap_priv.wl = realloc (heap_priv.wl, heap_priv.wl_cap * sizeof(GCObjectPtr)); + } + heap_priv.wl[heap_priv.wl_count++] = p; +} + +// After memcpy'ing a cell, SELF-INTERIOR pointers still aim at the old +// cell (found the hard way: every inline-buffer flat string's data +// pointed at poison after promotion). The two classes in the runtime: +// flat strings without an out-of-line buffer (data.flat = self+hdr) and +// small EJSArguments (args = self+sizeof). Anything new that embeds a +// self-pointer must be added here — the planned trace-bitmap redesign +// subsumes this with offset-based addressing. +void +minor_fixup_evacuated(GCObjectPtr from, GCObjectPtr to, size_t cell_size) +{ + GCObjectHeader h = *(GCObjectHeader*)to; + if (h & EJS_SCAN_TYPE_PRIMSTR) { + EJSPrimString* s = (EJSPrimString*)to; + if (EJS_PRIMSTR_GET_TYPE(s) == EJS_STRING_FLAT) { + char* d = (char*)s->data.flat; + if (d >= (char*)from && d < (char*)from + cell_size) + s->data.flat = (jschar*)((char*)to + (d - (char*)from)); + } + } + else if (h & EJS_SCAN_TYPE_OBJECT) { + EJSObject* o = (EJSObject*)to; + if (o->ops == &_ejs_Arguments_specops) { + EJSArguments* a = (EJSArguments*)o; + char* d = (char*)a->args; + if (d >= (char*)from && d < (char*)from + cell_size) + a->args = (ejsval*)((char*)to + (d - (char*)from)); + } + // shaped ordinary objects with EMBEDDED slot storage (gc-P5 + // single-cell allocation): the slots ejsval points into the + // cell. Shape bits are only ever set on ordinary objects, so + // the header test suffices; dictionary mode (shape 0) keeps + // the map pointer in the union and must not be touched. + else if (((h >> EJS_GC_HEADER_SHAPE_SHIFT) & EJS_GC_HEADER_SHAPE_MASK) + != EJS_SHAPE_DICT + && !EJSVAL_IS_NULL(o->slots)) { + char* d = (char*)EJSVAL_TO_CLOSUREENV_IMPL(o->slots); + if (d >= (char*)from && d < (char*)from + cell_size) + rewrite_slot_payload(&o->slots, + (GCObjectPtr)((char*)to + (d - (char*)from))); + } + } +} + +// allocate an old-gen cell for a promotion. Never triggers collection +// (we are inside one); grows a new arena if need be, aborts loudly on +// genuine OOM. +static GCObjectPtr +old_alloc_cell_for_promotion(size_t cell_size) +{ + int bucket = ffs((int)cell_size) - OBJECT_SIZE_LOW_LIMIT_BITS; + PageInfo* info = (PageInfo*)heap_pages[bucket].head; + while (info && !info->num_free_cells) info = info->next; + if (!info) { + info = alloc_new_page(cell_size); + if (info == NULL) { + _ejs_log ("gc: promotion allocation failed (size %zd)\n", cell_size); + abort(); + } + _ejs_list_prepend_node (&heap_pages[bucket], (EJSListNode*)info); + } + GCObjectPtr rv = alloc_from_page(info); + return rv; +} + +// conservative hit during a minor collection: young targets pin in +// place (never move this cycle) and join the scan worklist once; old +// targets are not this collection's problem +void +minor_conservative_hit(PageInfo* page, uint32_t cell_idx) +{ + if (!page->young) return; + if (page->young == 1 && !young_cell_is_allocated(page, cell_idx)) return; + if (page->young == 2 && cell_is_free(page->page_bitmap[cell_idx])) return; + BitmapCell cell = page->page_bitmap[cell_idx]; + if (cell_is_black(cell)) return; // already pinned this minor + GCObjectPtr base = page->page_start + ((size_t)cell_idx * page->cell_size); + if (_ejs_gc_is_forwarded(base)) return; // pins precede evacuation; stale hit + cell_set_black(&page->page_bitmap[cell_idx]); + heap_priv.minor_pins++; + MINOR_SPEW("minor: pin %p\n", base); + gc_watch_hit ("pin", base); + minor_wl_push(base); +} + +// how many young referents the current minor's precise frame walk +// EVACUATED (as opposed to found pinned/forwarded/old) — the direct +// measure that precision is actually moving things (EJS_GC_PROFILE) +static uint64_t gc_frame_moves; + +// the minor collection's slot callback (the slot-protocol payoff: every precise +// scan — roots, modules, remset, transitive object scan — goes through +// here). Young referents evacuate (or stay pinned); the slot is +// rewritten to the object's final address. +static void +minor_process_slot(ejsval* slot) +{ + ejsval v = *slot; + if (!EJSVAL_IS_TRACEABLE_IMPL(v)) return; + GCObjectPtr p = (GCObjectPtr)EJSVAL_TO_GCTHING_IMPL(v); + if (p == NULL || !_ejs_gc_is_young(p)) return; + + uint32_t cell_idx; + PageInfo* page = find_page_and_cell(p, &cell_idx); + EJS_ASSERT(page && page->young); + GCObjectPtr base = page->page_start + ((size_t)cell_idx * page->cell_size); + + if (_ejs_gc_is_forwarded(base)) { + rewrite_slot_payload(slot, _ejs_gc_forwarding_addr(base)); + return; + } + if (cell_is_black(page->page_bitmap[cell_idx])) { + // pinned: stays put, already queued for scanning. The current + // owner must stay dirty so the edge is revisited next cycle. + minor_scan_saw_young = EJS_TRUE; + return; + } + + // evacuate: copy the whole cell, clear YOUNG on the copy (it is + // promoted), forward the old cell, rewrite this slot + GCObjectPtr to = old_alloc_cell_for_promotion(page->cell_size); + memcpy (to, base, page->cell_size); + // promoted: not young; and not DIRTY — the memcpy'd bit would make + // the carry logic think the copy is already queued (it is not) + *(GCObjectHeader*)to &= ~(EJS_GC_HEADER_YOUNG | EJS_GC_HEADER_DIRTY); + minor_fixup_evacuated(base, to, page->cell_size); + _ejs_gc_forward(base, to); + rewrite_slot_payload(slot, to); + gc_watch_hit ("evacuate-from", base); + MINOR_SPEW("minor: evac %p -> %p (hdr %llx)\n", base, to, (unsigned long long)*(GCObjectHeader*)to); + heap_priv.promoted_objs++; + heap_priv.promoted_bytes += page->cell_size; + minor_wl_push(to); +} + +// evacuate/pin-resolve a RAW GC pointer field (rope/dependent string +// children — the only raw object->object pointers in the heap) +static void +minor_process_primstr_child(EJSPrimString** childp) +{ + GCObjectPtr p = (GCObjectPtr)*childp; + if (p == NULL || !_ejs_gc_is_young(p)) return; + uint32_t cell_idx; + PageInfo* page = find_page_and_cell(p, &cell_idx); + EJS_ASSERT(page && page->young); + GCObjectPtr base = page->page_start + ((size_t)cell_idx * page->cell_size); + if (_ejs_gc_is_forwarded(base)) { + *childp = (EJSPrimString*)_ejs_gc_forwarding_addr(base); + return; + } + if (cell_is_black(page->page_bitmap[cell_idx])) { minor_scan_saw_young = EJS_TRUE; return; } + GCObjectPtr to = old_alloc_cell_for_promotion(page->cell_size); + memcpy (to, base, page->cell_size); + // promoted: not young; and not DIRTY — the memcpy'd bit would make + // the carry logic think the copy is already queued (it is not) + *(GCObjectHeader*)to &= ~(EJS_GC_HEADER_YOUNG | EJS_GC_HEADER_DIRTY); + minor_fixup_evacuated(base, to, page->cell_size); + _ejs_gc_forward(base, to); + *childp = (EJSPrimString*)to; + MINOR_SPEW("minor: evac-child %p -> %p\n", base, to); + heap_priv.promoted_objs++; + heap_priv.promoted_bytes += page->cell_size; + minor_wl_push(to); +} + +// scan one object's outgoing edges with minor_process_slot — the exact +// shape of process_worklist's dispatch, on the slot-based protocol +static void +minor_scan_object(GCObjectPtr p) +{ + GCObjectHeader header = *(GCObjectHeader*)p; + if ((header & EJS_SCAN_TYPE_OBJECT) != 0) { + EJSObject* obj = (EJSObject*)p; + if (obj->ops != NULL) + OP(obj,Scan)(obj, minor_process_slot); + } + else if ((header & EJS_SCAN_TYPE_PRIMSTR) != 0) { + EJSPrimString* primStr = (EJSPrimString*)p; + EJSBool child_still_young = EJS_FALSE; + switch (EJS_PRIMSTR_GET_TYPE(primStr)) { + case EJS_STRING_ROPE: + minor_process_primstr_child(&primStr->data.rope.left); + minor_process_primstr_child(&primStr->data.rope.right); + child_still_young = _ejs_gc_is_young(primStr->data.rope.left) + || _ejs_gc_is_young(primStr->data.rope.right); + break; + case EJS_STRING_DEPENDENT: + minor_process_primstr_child(&primStr->data.dependent.dep); + child_still_young = _ejs_gc_is_young(primStr->data.dependent.dep); + break; + case EJS_STRING_FLAT: + break; + } + if (child_still_young) + minor_scan_saw_young = EJS_TRUE; + } + else if ((header & EJS_SCAN_TYPE_PRIMSYM) != 0) { + minor_process_slot(&((EJSPrimSymbol*)p)->description); + } + else if ((header & EJS_SCAN_TYPE_CLOSUREENV) != 0) { + EJSClosureEnv* env = (EJSClosureEnv*)p; + for (uint32_t i = 0; i < env->length; i++) + minor_process_slot(&env->slots[i]); + } +} + +// the overflow fallback scans every live old object — it must maintain +// the same DIRTY-bit discipline as normal processing (clear, scan, +// re-dirty on remaining pinned-young refs), or bits desync from the +// swapped-away buffer and later stores skip re-queuing forever +static void +minor_scan_object_if_live(GCObjectPtr p) +{ + *(GCObjectHeader*)p &= ~EJS_GC_HEADER_DIRTY; + minor_scan_saw_young = EJS_FALSE; + minor_scan_object(p); + if (minor_scan_saw_young) + _ejs_gc_remember_slow(p); +} + +// A FULL collection frees dead old objects, so every remset/rescan +// entry — slots INTERIOR to old cells — may now dangle into poisoned +// memory (found as 0xfffc_afaf… "object-tagged poison" values read by +// the next minor). Rebuild the whole remembered state from a live +// old-gen walk instead: record every live old→young ejsval slot, re-add +// old strings with young raw children, and drop the LOS-pending list +// (the walk covers LOS objects). Full collections are rare; one extra +// old-gen walk apiece is cheap insurance. +void +remset_rebuild_after_full_gc(void) +{ + if (!nursery_enabled) return; + // entries are heap OBJECTS: drop the ones the sweep freed, keep the + // rest (their DIRTY bits are still set) + int kept = 0; + for (int i = 0; i < _ejs_heap.remset_count; i++) { + GCObjectPtr o = (GCObjectPtr)_ejs_heap.remset[i]; + uint32_t ci; + PageInfo* pg = find_page_and_cell(o, &ci); + if (!pg || !cell_is_allocated(pg, ci, pg->page_bitmap[ci])) + continue; + _ejs_heap.remset[kept++] = _ejs_heap.remset[i]; + } + _ejs_heap.remset_count = kept; +} + +void +_ejs_gc_minor_collect(const char* reason) +{ + struct timeval tv0, tv1; + gettimeofday (&tv0, NULL); + + if (in_minor_gc) { + _ejs_log ("GC BUG: reentrant minor collection (reason=%s)\n", reason); + abort(); + } + + // everything below this frame is collector machinery: the paranoid + // checker's raw-stack sweep must not read it (see ejs-gc-debug.c) + if (gc_paranoid) + paranoid_stack_floor = (void**)__builtin_frame_address(0); + + young_flush_bumps(); + + in_minor_gc = EJS_TRUE; + heap_priv.minors++; + MINOR_SPEW("minor: begin %llu\n", (unsigned long long)heap_priv.minors); + uint64_t promoted_objs_before = heap_priv.promoted_objs; + uint64_t promoted_bytes_before = heap_priv.promoted_bytes; + uint64_t pins_before = heap_priv.minor_pins; + int remset_used = _ejs_heap.remset_count; + EJSBool overflowed = _ejs_heap.remset_overflowed != 0; + if ((uint64_t)_ejs_heap.remset_count > heap_priv.remset_peak) + heap_priv.remset_peak = _ejs_heap.remset_count; + + // 0. swap the remset buffers up front: EVERY minor_process_slot call + // from here on (roots, modules, remset snapshot, transitive scan) + // may carry an old→pinned-young edge into the LIVE buffer for the + // next cycle — the snapshot is what this cycle processes + void** snapshot = _ejs_heap.remset; + int snapshot_count = _ejs_heap.remset_count; + EJSBool snapshot_overflowed = _ejs_heap.remset_overflowed != 0; + _ejs_heap.remset = heap_priv.remset_other; + heap_priv.remset_other = snapshot; + _ejs_heap.remset_count = 0; + _ejs_heap.remset_overflowed = 0; + + // 1. conservative pins FIRST: C stacks, registers, and EVERY live + // generator's suspended stack + saved contexts (the registry + // walk) — all ambiguous references must pin before any object + // moves; a generator discovered mid-trace would pin too late. + // The shared mark helpers dispatch to minor_conservative_hit + // while in_minor_gc is set. + struct timeval ph0, ph1, ph2, ph3, ph4, ph5; + int gen_count = 0; + gettimeofday (&ph0, NULL); + // each conservative range scan skips the gc-frame records of + // the stack it is scanning — those slots are precise roots, and + // seeing them conservatively would pin every frame-held value + // through its own slot (precision would never move anything) + set_frame_skip_chain(_ejs_heap.gc_frame_head); + mark_thread_stack(); + mark_generator_stacks(); + for (EJSGenerator* g = _ejs_generator_registry; g; g = g->reg_next) { + set_frame_skip_chain(g->gc_frame_head); + _ejs_generator_scan_conservative(g); + gen_count++; + } + clear_frame_skip(); + gettimeofday (&ph1, NULL); + + // 1.5 the emitted gc-frame chains — precise, relocatable + // JS-frame roots. Runs AFTER the conservative pins on purpose: + // an object visible to both a gc-frame slot and a C frame (an + // ejsval argument into the very runtime call that triggered this + // minor, say) is pinned, and minor_process_slot leaves pinned + // targets in place — the pin must win or the C frame's copy + // dangles. Everything frame-held and NOT C-visible evacuates + // and gets its slot rewritten. + { + uint64_t promoted_before_frames = heap_priv.promoted_objs; + walk_gc_frames(minor_process_slot); + gc_frame_moves = heap_priv.promoted_objs - promoted_before_frames; + } + + // 2. precise roots: the root registry and module exports evacuate + root_registry_foreach (minor_process_slot); + for (int i = 0; i < _ejs_num_modules; i++) { + EJSObject* mod = (EJSObject*)_ejs_modules[i]; + if (mod->ops == NULL) continue; + OP(mod,Scan)(mod, minor_process_slot); + } + gettimeofday (&ph2, NULL); + + // 3. the remembered set snapshot (or, after overflow, every live + // old object) + if (snapshot_overflowed) { + heap_priv.overflow_minors++; + old_gen_walk (minor_scan_object_if_live); + } else { + for (int i = 0; i < snapshot_count; i++) { + GCObjectPtr owner = (GCObjectPtr)snapshot[i]; + // the object may have died and been swept by an interleaved + // FULL collection; its cell reads FREE then — skip. (A + // reused cell scans as whatever lives there now: merely + // conservative.) + uint32_t ci; + PageInfo* pg = find_page_and_cell(owner, &ci); + if (!pg || !cell_is_allocated(pg, ci, pg->page_bitmap[ci])) + continue; + *(GCObjectHeader*)owner &= ~EJS_GC_HEADER_DIRTY; + minor_scan_saw_young = EJS_FALSE; + minor_scan_object(owner); + // still holds pinned-young references: stay dirty + if (minor_scan_saw_young) + _ejs_gc_remember_slow(owner); + } + } + + // 4. transitive closure. Objects scanned here (promoted copies, + // pinned young, generator roots) that still reference pinned- + // young data must carry a dirty mark so the next cycle revisits + // them (young owners filter out inside remember). + gettimeofday (&ph3, NULL); + while (heap_priv.wl_count > 0) { + GCObjectPtr o = heap_priv.wl[--heap_priv.wl_count]; + minor_scan_saw_young = EJS_FALSE; + minor_scan_object (o); + if (minor_scan_saw_young && !_ejs_gc_is_young(o) + && !(*(GCObjectHeader*)o & EJS_GC_HEADER_DIRTY)) + _ejs_gc_remember_slow(o); + } + gettimeofday (&ph4, NULL); + + // 5. optional barrier-coverage verification + if (heap_priv.verify && !snapshot_overflowed) { + verify_bad_slot = NULL; + old_gen_walk (verify_check_object); + // generator specops re-run their conservative scans inside the + // verify walk (side effect: fresh pins pushed on the worklist); + // drain them before the sweep decides survivor pages + while (heap_priv.wl_count > 0) + minor_scan_object (heap_priv.wl[--heap_priv.wl_count]); + } + + // 6. sweep the young pages: dead cells finalize; forwarded cells are + // just space; pages with pins become survivor pages, the rest reset + for (int i = 0; i < EJS_GC_NUM_SIZE_CLASSES; i++) + young_page_retire_current(i); + + EJSList survivor_pages; + memset (&survivor_pages, 0, sizeof(survivor_pages)); + PageInfo* page; + while ((page = (PageInfo*)heap_priv.young_pages.head) != NULL) { + for (int sc = 0; sc < EJS_GC_NUM_SIZE_CLASSES; sc++) { + if (heap_priv.young_current[sc] == page + || ((char*)_ejs_heap.bump[sc] > (char*)page->page_start + && (char*)_ejs_heap.bump[sc] <= (char*)page->page_end)) { + _ejs_log ("GC BUG: sweeping page %p that is still active for class %d (bump=%p)\n", + page->page_start, sc, _ejs_heap.bump[sc]); + abort(); + } + } + int survivors = 0; + GCObjectPtr p = page->page_start; + for (int c = 0; c < CELLS_IN_PAGE(page); c++, p += page->cell_size) { + EJSBool allocated = (page->young == 1) + ? young_cell_is_allocated(page, (uint32_t)c) + : !cell_is_free(page->page_bitmap[c]); + if (!allocated) { cell_set_free(&page->page_bitmap[c]); continue; } + if (_ejs_gc_is_forwarded(p)) { + // evacuated: the space is reusable; poison it now that + // every slot has been processed + gc_watch_hit ("sweep-poison-forwarded", p); + memset (p, 0xa7, page->cell_size); // NOT 0xaf: bit 59 (FORWARDED) must stay clear in poison + cell_set_free(&page->page_bitmap[c]); + continue; + } + if (cell_is_black(page->page_bitmap[c])) { + // pinned survivor: stays young, stays put; back to white + // so the next cycle (minor or full) sees it fresh + cell_set_white(&page->page_bitmap[c]); + cell_set_allocated(&page->page_bitmap[c]); + survivors++; + continue; + } + MINOR_SPEW("minor: free %p (hdr %llx)\n", p, (unsigned long long)*(GCObjectHeader*)p); + if (gc_paranoid) { + // who still references this about-to-die young object? + // (reverse lookup across every location the minor is + // supposed to have processed) + if (paranoid_report_referrers(p) > 0) + abort(); + } + gc_watch_hit ("sweep-poison-dead", p); + finalize_object(p); + memset (p, 0xa7, page->cell_size); // NOT 0xaf: bit 59 (FORWARDED) must stay clear in poison + cell_set_free(&page->page_bitmap[c]); + } + _ejs_list_detach_node (&heap_priv.young_pages, (EJSListNode*)page); + if (survivors == 0) { + page->young = 0; + page->bump_ptr = page->page_start; + page->num_free_cells = page->num_cells; + EJS_LIST_PREPEND(page, heap_priv.nursery_arena->free_pages); + } else { + page->young = 2; + page->num_free_cells = page->num_cells - survivors; + _ejs_list_append_node (&survivor_pages, (EJSListNode*)page); + } + } + heap_priv.young_pages = survivor_pages; + gettimeofday (&ph5, NULL); + + // 7. cycle accounting (the remset swapped/reset in step 0; carried + // edges are already in the live buffer); promoted bytes feed the + // FULL collection trigger (they are old-gen growth) + heap_priv.young_alloced = 0; + alloc_size += heap_priv.promoted_bytes - promoted_bytes_before; + + // seam/private-state consistency: every class was retired in step 6; + // nothing may have reinstalled a bump cursor mid-minor + for (int sc = 0; sc < EJS_GC_NUM_SIZE_CLASSES; sc++) { + if (_ejs_heap.bump[sc] != NULL || heap_priv.young_current[sc] != NULL) { + _ejs_log ("GC BUG: minor end: class %d seam desync (bump=%p current=%p)\n", + sc, _ejs_heap.bump[sc], (void*)heap_priv.young_current[sc]); + abort(); + } + } + + MINOR_SPEW("minor: end %llu\n", (unsigned long long)heap_priv.minors); + in_minor_gc = EJS_FALSE; + + gettimeofday (&tv1, NULL); + uint64_t usec = (tv1.tv_sec - tv0.tv_sec) * 1000000ULL + (tv1.tv_usec - tv0.tv_usec); + heap_priv.minor_usec_total += usec; + if (usec > heap_priv.minor_usec_max) heap_priv.minor_usec_max = usec; + if (gc_paranoid) + paranoid_sweep_check(); + if (gc_profile) { +#define PHUS(a,b) (((b).tv_sec - (a).tv_sec) * 1000000LL + ((b).tv_usec - (a).tv_usec)) + _ejs_log ("EJS_GC_PROFILE: minor#%llu reason=%s pause=%.3fms promoted=%llu/%lluKB pins=%llu gcframe_moves=%llu remset=%d gens=%d phases[pins=%lld roots=%lld dirty=%lld wl=%lld sweep=%lld]us%s\n", + (unsigned long long)heap_priv.minors, reason, usec / 1000.0, + (unsigned long long)(heap_priv.promoted_objs - promoted_objs_before), + (unsigned long long)((heap_priv.promoted_bytes - promoted_bytes_before) / 1024), + (unsigned long long)(heap_priv.minor_pins - pins_before), + (unsigned long long)gc_frame_moves, + remset_used, gen_count, + (long long)PHUS(ph0,ph1), (long long)PHUS(ph1,ph2), (long long)PHUS(ph2,ph3), + (long long)PHUS(ph3,ph4), (long long)PHUS(ph4,ph5), + overflowed ? " OVERFLOW" : ""); +#undef PHUS + } + + // promotions grow the old gen; the policy may schedule a full + gc_policy (GC_POLICY_AFTER_MINOR, NULL); +} + +// the young allocation slow path: refill the class's bump page, running +// a minor collection when the nursery is exhausted +GCObjectPtr +young_alloc_slow(int idx, size_t cell_size, EJSScanType scan_type) +{ + young_page_retire_current(idx); + // the budget bounds the per-minor sweep (pause target <1ms) — the + // arena is the hard capacity, the budget the soft trigger + if (heap_priv.young_alloced >= heap_priv.young_budget) + _ejs_gc_minor_collect("nursery budget"); + if (!young_page_install(idx, cell_size)) { + _ejs_gc_minor_collect("nursery exhausted"); + if (!young_page_install(idx, cell_size)) { + // nursery still full (all survivor pages): give up on the + // nursery for this allocation and take the old path + return NULL; + } + } + void* p = _ejs_heap.bump[idx]; + _ejs_heap.bump[idx] = (char*)p + cell_size; + memset (p, 0, cell_size); + *(GCObjectHeader*)p = scan_type | EJS_GC_HEADER_YOUNG; + return p; +} + +// Full collections see young pages too. Active (bump-rule) pages have +// no valid FREE bits or num_free_cells, so normalize them to +// bitmap-authoritative survivor form first: cells below the bump are +// allocated, the rest free, and the page leaves bump service. After +// this the existing mark/sweep machinery handles them verbatim (their +// objects remain YOUNG by address range; the next minor collection +// evacuates or re-pins whatever survives the full GC). +void +young_normalize_for_full_gc(void) +{ + if (!nursery_enabled) return; + young_flush_bumps(); + for (int i = 0; i < EJS_GC_NUM_SIZE_CLASSES; i++) + young_page_retire_current(i); + for (PageInfo* page = (PageInfo*)heap_priv.young_pages.head; page; page = page->next) { + if (page->young != 1) continue; + int allocated = 0; + for (int c = 0; c < CELLS_IN_PAGE(page); c++) { + if (young_cell_is_allocated(page, (uint32_t)c)) { + cell_set_allocated(&page->page_bitmap[c]); + allocated++; + } else { + cell_set_free(&page->page_bitmap[c]); + } + } + page->num_free_cells = page->num_cells - allocated; + page->young = 2; + } + heap_priv.young_alloced = 0; +} + +void +nursery_init(void) +{ + // nursery ON by default (gate decision 2026-07-25); + // EJS_GC_NURSERY=off (or =0) selects the old collector for A/B. + { + char* e = getenv("EJS_GC_NURSERY"); + nursery_enabled = !(e && (strcmp(e, "off") == 0 || strcmp(e, "0") == 0)); + } + heap_priv.verify = getenv("EJS_GC_VERIFY") != NULL; + minor_spew = getenv("EJS_GC_MINOR_SPEW") != NULL; + gc_paranoid = getenv("EJS_GC_PARANOID") != NULL; + if (getenv("EJS_GC_WATCH")) + gc_watch_addr = (uintptr_t)strtoull(getenv("EJS_GC_WATCH"), NULL, 16); + // 1MB balances pause and throughput (measured 2026-07-25): minor p99 + // ~1.3ms on the bench corpus (512KB reaches 0.68ms at ~10% self- + // compile cost; 4MB buys self-compile ~3% at ~5ms p99) + heap_priv.young_budget = 1024 * 1024; + char* budget_env = getenv("EJS_GC_NURSERY_BUDGET"); + if (budget_env) heap_priv.young_budget = (size_t)atoll(budget_env); + if (!nursery_enabled) return; + + Arena* arena = arena_new(); + if (!arena) { + _ejs_log ("gc: could not allocate the nursery arena; nursery disabled\n"); + nursery_enabled = EJS_FALSE; + return; + } + arena->is_nursery = EJS_TRUE; + heap_priv.nursery_arena = arena; + _ejs_heap.nursery_base = (void*)arena; + _ejs_heap.nursery_end = arena->end; + _ejs_heap.remset = malloc (NURSERY_REMSET_CAPACITY * sizeof(void*)); + _ejs_heap.remset_capacity = NURSERY_REMSET_CAPACITY; + heap_priv.remset_other = malloc (NURSERY_REMSET_CAPACITY * sizeof(void*)); +} +// ===================== end nursery ========================================= diff --git a/runtime/ejs-gc.c b/runtime/ejs-gc.c index def1e93a..8dfb82d8 100644 --- a/runtime/ejs-gc.c +++ b/runtime/ejs-gc.c @@ -2,600 +2,132 @@ * vim: set ts=4 sw=4 et tw=99 ft=cpp: */ -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "ejs-gc.h" -#include "ejs-function.h" -#include "ejs-generator.h" -#include "ejs-value.h" -#include "ejs-string.h" -#include "ejs-symbol.h" -#include "ejs-error.h" -#include "ejs-ops.h" -#include "ejsval.h" -#include "ejs-module.h" - -#define clear_on_finalize 0 - -#define spew 0 -#define sanity 0 -#define gc_timings 0 - -#if spew -static int _ejs_spew_level = (spew); -#define SPEW(level,x) do { if ((level) < _ejs_spew_level) { x; } } while (0) -#else -#define SPEW(level,x) -#endif -#if sanity -#define SANITY(x) x -#else -#define SANITY(x) -#endif +// the collector core: lifecycle API (init/alloc/shutdown), the cell +// free path, the root registry, the collection policy, the write- +// barrier entry points, and the GC JS object. The module map lives +// in ejs-gc-internal.h. + +#include "ejs-gc-internal.h" // exceptions to throw if we're out of memory static ejsval los_allocation_failed_exc EJSVAL_ALIGNMENT; static ejsval page_allocation_failed_exc EJSVAL_ALIGNMENT; -void _ejs_gc_dump_heap_stats(); - -#if EJS_BITS_PER_WORD == 64 -// 2GB -#define MAX_HEAP_SIZE (2LL * 1024LL * 1024LL * 1024LL) -#else -// 128MB -#define MAX_HEAP_SIZE (128LL * 1024LL * 1024LL) -#endif - -#ifndef PAGE_SIZE -#define PAGE_SIZE 4096 -#endif - -#define USABLE_PAGE_SIZE PAGE_SIZE - -#define CELLS_OF_SIZE(size) (USABLE_PAGE_SIZE / (size)) -#define CELLS_IN_PAGE(page) CELLS_OF_SIZE((page)->cell_size) - -// arenas are reserved in ARENA_PAGES * PAGE_SIZE chunks. ARENA_PAGES=4096 gives us an arena size of 32MB -#define ARENA_PAGES 8192 -#define ARENA_SIZE (PAGE_SIZE*ARENA_PAGES) - -#define PTR_TO_ARENA_MASK (uintptr_t)(~(ARENA_SIZE-1)) - -// turn a random pointer into an arena pointer -#define PTR_TO_ARENA(ptr) ((void*)((uintptr_t)(ptr) & PTR_TO_ARENA_MASK)) -#define PTR_TO_ARENA_PAGE_BASE(ptr) ((void*)EJS_ALIGN(PTR_TO_ARENA(ptr) + sizeof(Arena), PAGE_SIZE)) -#define PTR_TO_ARENA_PAGE_INDEX(ptr) ((((uintptr_t)(ptr) & ~PTR_TO_ARENA_MASK) - ((uintptr_t)PTR_TO_ARENA_PAGE_BASE(ptr) & ~PTR_TO_ARENA_MASK)) / PAGE_SIZE) - -#define PTR_TO_CELL(ptr,info) (((char*)(ptr) - (char*)(info)->page_start) / (info)->cell_size) - -#define OBJ_TO_PAGE(o) ((o) & ~PAGE_SIZE) - -#define IS_ALIGNED_TO(v,a) (((uintptr_t)(v) & ((a)-1)) == 0) -#define ALLOC_ALIGN 8 -#define EJS_ALIGN(v,a) (((uintptr_t)(v) + (a)-1) & ~((a)-1)) -#define IS_ALLOC_ALIGNED(v) IS_ALIGNED_TO(v, ALLOC_ALIGN) - -#if IOS || OSX -#include -#define MAP_FD VM_MAKE_TAG (VM_MEMORY_APPLICATION_SPECIFIC_16) -#else -#define MAP_FD -1 -#endif - EJSBool gc_disabled; int collect_every_alloc = 0; -#if CONCURRENT -#error "not implemented" -#else -#define LOCK_PAGE(info) -#define UNLOCK_PAGE(info) -#define LOCK_GC() -#define UNLOCK_GC() -#define LOCK_ARENAS() -#define UNLOCK_ARENAS() -#endif - - -#define MAX_WORKLIST_SEGMENT_SIZE 512 -typedef struct _WorkListSegmnt { - EJS_SLIST_HEADER(struct _WorkListSegmnt); - int size; - GCObjectPtr work_list[MAX_WORKLIST_SEGMENT_SIZE]; -} WorkListSegment; - -typedef struct { - WorkListSegment *list; - WorkListSegment *free_list; -} WorkList; - -static WorkList work_list; - -static void -_ejs_gc_worklist_init() -{ - work_list.list = NULL; - work_list.free_list = NULL; -} - -static void -_ejs_gc_worklist_push(GCObjectPtr obj) -{ - if (obj == NULL) - return; - - WorkListSegment *segment; - - if (EJS_UNLIKELY(!work_list.list || work_list.list->size == MAX_WORKLIST_SEGMENT_SIZE)) { - // we need a new segment - if (work_list.free_list) { - // take one from the free list - segment = work_list.free_list; - EJS_SLIST_DETACH_HEAD(segment, work_list.free_list); - } - else { - segment = (WorkListSegment*)malloc (sizeof(WorkListSegment)); - segment->size = 0; - } - EJS_SLIST_ATTACH(segment, work_list.list); - } - else { - segment = work_list.list; - } - - segment->work_list[segment->size++] = obj; -} - -static GCObjectPtr -_ejs_gc_worklist_pop() -{ - if (work_list.list == NULL || work_list.list->size == 0/* shouldn't happen, since we push the page to the free list if we hit 0 */) - return NULL; - - WorkListSegment *segment = work_list.list; - - GCObjectPtr rv = segment->work_list[--segment->size]; - if (segment->size == 0) { - EJS_SLIST_DETACH_HEAD(segment, work_list.list); - EJS_SLIST_ATTACH(segment, work_list.free_list); - } - return rv; -} - -#define WORKLIST_PUSH_AND_GRAY(x) EJS_MACRO_START \ - if (is_white((GCObjectPtr)x)) { \ - _ejs_gc_worklist_push((GCObjectPtr)(x)); \ - set_gray ((GCObjectPtr)(x)); \ - } \ - EJS_MACRO_END - -#define WORKLIST_PUSH_AND_GRAY_CELL(x, cell) EJS_MACRO_START \ - if (IS_WHITE(cell)) { \ - _ejs_gc_worklist_push((GCObjectPtr)(x)); \ - SET_GRAY (cell); \ - } \ - EJS_MACRO_END - -typedef struct _RootSetEntry { - EJS_LIST_HEADER(struct _RootSetEntry); - ejsval* root; -} RootSetEntry; - -static RootSetEntry *root_set; - -static void* -alloc_from_os(size_t size, size_t align) -{ - if (align == 0) { - size = MAX(size, PAGE_SIZE); - void* res = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE, MAP_FD, 0); - SPEW(2, _ejs_log ("mmap for 0 alignment = %p\n", res)); - return res == MAP_FAILED ? NULL : res; - } - - void* res = mmap(NULL, size*2, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE, MAP_FD, 0); - if (res == MAP_FAILED) { - return NULL; - } - - SPEW(2, _ejs_log ("mmap returned %p\n", res)); - - if (((uintptr_t)res % align) == 0) { - // the memory was aligned, unmap the second half of our mapping - // XXX should we just rejoice and add both halves? - SPEW(2, _ejs_log ("already aligned\n")); - munmap (res + size, size); - } - else { - SPEW(2, _ejs_log ("not aligned\n")); - // align res, and unmap the areas before/after the new mapping - void *aligned_res = (void*)EJS_ALIGN(res, align); - // the area before - munmap (res, (uintptr_t)aligned_res - (uintptr_t)res); - // the area after - munmap (aligned_res+size, (uintptr_t)res+size*2 - (uintptr_t)(aligned_res+size)); - res = aligned_res; - SPEW(2, _ejs_log ("aligned ptr = %p\n", res)); - } - return res; -} - -static void -release_to_os(void* ptr, size_t size) -{ - munmap (ptr, size); -} - -typedef struct _LargeObjectInfo LargeObjectInfo; -static void release_to_los (LargeObjectInfo *lobj); - -typedef struct _PageInfo PageInfo; -typedef struct _Arena { - void* end; - void* pos; - PageInfo* free_pages; - void* pages[ARENA_PAGES]; - PageInfo* page_infos[ARENA_PAGES]; - int num_pages; -} Arena; - -#define MAX_ARENAS (MAX_HEAP_SIZE / ARENA_SIZE) -static Arena *heap_arenas[MAX_ARENAS]; -static int num_arenas; - -typedef char BitmapCell; - -#define CELL_COLOR_MASK 0x03 -#define CELL_GRAY_MASK 0x02 -#define CELL_WHITE_MASK_START 0x00 -#define CELL_BLACK_MASK_START 0x01 -#define CELL_FREE 0x04 // cell is in the free list for this page - -static unsigned int black_mask = CELL_BLACK_MASK_START; -static unsigned int white_mask = CELL_WHITE_MASK_START; - -#if CONCURRENT -#define SET_GRAY(cell) EJS_MACRO_START \ - BitmapCell _bc; \ - do { \ - _bc = (cell); \ - } while (!__sync_bool_compare_and_swap (&cell, _bc, (_bc & ~CELL_COLOR_MASK) | CELL_GRAY_MASK)); \ - EJS_MACRO_END - -#define SET_WHITE(cell) EJS_MACRO_START \ - BitmapCell _bc; \ - do { \ - _bc = (cell); \ - } while (!__sync_bool_compare_and_swap (&cell, _bc, (_bc & ~CELL_COLOR_MASK) | white_mask)); \ - EJS_MACRO_END - -#define SET_BLACK(cell) EJS_MACRO_START \ - BitmapCell _bc; \ - do { \ - _bc = (cell); \ - } while (!__sync_bool_compare_and_swap (&cell, _bc, (_bc & ~CELL_COLOR_MASK) | black_mask)); \ - EJS_MACRO_END - -#define SET_FREE(cell) EJS_MACRO_START \ - BitmapCell _bc; \ - do { \ - _bc = (cell); \ - } while (!__sync_bool_compare_and_swap (&cell, _bc, CELL_FREE)); \ - EJS_MACRO_END - -#define SET_ALLOCATED(cell) EJS_MACRO_START \ - BitmapCell _bc; \ - do { \ - _bc = (cell); \ - } while (!__sync_bool_compare_and_swap (&cell, _bc, (_bc & ~CELL_FREE))); \ - EJS_MACRO_END -#else -#define SET_GRAY(cell) (cell) = (((cell) & ~CELL_COLOR_MASK) | CELL_GRAY_MASK) -#define SET_WHITE(cell) (cell) = (((cell) & ~CELL_COLOR_MASK) | white_mask) -#define SET_BLACK(cell) (cell) = (((cell) & ~CELL_COLOR_MASK) | black_mask) -#define SET_FREE(cell) (cell) = CELL_FREE -#define SET_ALLOCATED(cell) (cell) = ((cell) & ~CELL_FREE) -#endif - -#define IS_FREE(cell) (((cell) & CELL_FREE) == CELL_FREE) -#define IS_GRAY(cell) (((cell) & CELL_COLOR_MASK) == CELL_GRAY_MASK) -#define IS_WHITE(cell) (((cell) & CELL_COLOR_MASK) == white_mask) -#define IS_BLACK(cell) (((cell) & CELL_COLOR_MASK) == black_mask) - -struct _PageInfo { - EJS_LIST_HEADER(struct _PageInfo); - void* bump_ptr; - void* page_start; - void* page_end; - BitmapCell* page_bitmap; - LargeObjectInfo *los_info; - int32_t cell_size; - int16_t num_cells; - int16_t num_free_cells; -}; - -struct _LargeObjectInfo { - EJS_LIST_HEADER(struct _LargeObjectInfo); - size_t alloc_size; - PageInfo page_info; -}; - -#define OBJECT_SIZE_LOW_LIMIT_BITS 4 // smallest object we'll allocate (1<<4 = 16) -#define OBJECT_SIZE_HIGH_LIMIT_BITS 8 // max object size for the non-LOS allocator = 256 - -#define HEAP_PAGELISTS_COUNT (OBJECT_SIZE_HIGH_LIMIT_BITS - OBJECT_SIZE_LOW_LIMIT_BITS) + 1 // +1 because we're inclusive on OBJECT_SIZE_HIGH_LIMIT_BITS - -static EJSList heap_pages[HEAP_PAGELISTS_COUNT]; -static LargeObjectInfo *los_list; - -void* ptr_to_arena(void* ptr) { return PTR_TO_ARENA(ptr); } -void* ptr_to_arena_page_base(void* ptr) { return PTR_TO_ARENA_PAGE_BASE(ptr); } -uintptr_t ptr_to_arena_page_index(void* ptr) { return PTR_TO_ARENA_PAGE_INDEX(ptr); } -uintptr_t ptr_to_cell(void* ptr, PageInfo* info ) { return PTR_TO_CELL(ptr, info); } - -#if sanity -static void -verify_arena(Arena *arena) -{ - for (int i = 0; i < arena->num_pages; i ++) { - EJS_ASSERT (arena->pages[i] == arena->page_infos[i]->page_start); - } -} -#endif - - -static Arena* -arena_new() -{ - if (num_arenas == MAX_ARENAS-1) - return NULL; - - SPEW(1, _ejs_log ("num_arenas = %d, max = %d\n", num_arenas, MAX_ARENAS)); +// the cell-lifecycle epoch (ejs-gc-internal.h owns the encoding); +// parity 1 at startup so black starts at color 1 +unsigned int mark_epoch = 1; + +// ---- the root registry ----------------------------------------- +// +// Registered roots are the addresses of ejsval slots in static or +// malloc'd storage (atoms, well-knowns, the OOM exceptions). A +// growable array: O(1) add, swap-with-last remove, and ONE iteration +// helper every collector phase shares — full-GC mark, minor +// evacuation, compaction fixup, and the debug walks see the same set +// by construction. (The predecessor was a malloc'd linked list with +// five hand-rolled walks.) +static ejsval** root_registry; +static int root_registry_count; +static int root_registry_capacity; - void* arena_start = alloc_from_os(ARENA_SIZE, ARENA_SIZE); - if (arena_start == NULL) - return NULL; - - Arena* new_arena = arena_start; - - memset (new_arena, 0, sizeof(Arena)); - - new_arena->end = arena_start + ARENA_SIZE; - new_arena->pos = (void*)EJS_ALIGN(arena_start + sizeof(Arena), PAGE_SIZE); - - LOCK_ARENAS(); - int insert_point = -1; - for (int i = 0; i < num_arenas; i ++) { - if (new_arena < heap_arenas[i]) { - insert_point = i; - break; - } - } - if (insert_point == -1) insert_point = num_arenas; - if (num_arenas-insert_point > 0) - memmove (&heap_arenas[insert_point + 1], &heap_arenas[insert_point], (num_arenas-insert_point)*sizeof(Arena*)); - heap_arenas[insert_point] = new_arena; - num_arenas++; - UNLOCK_ARENAS(); - - return new_arena; -} - -static void -arena_destroy (Arena* arena) +void +root_registry_foreach(void (*fn)(ejsval*)) { - release_to_os (arena, (intptr_t)arena->end - (intptr_t)arena); + for (int i = 0; i < root_registry_count; i++) + fn(root_registry[i]); } -static PageInfo* -alloc_page_info_from_arena(Arena *arena, void *page_data, size_t cell_size) +// the shutdown collection NULLs every root before the final sweep +void +root_registry_shutdown(void) { - // FIXME allocate the PageInfo and bitmap from the arena as well - PageInfo* info = (PageInfo*)calloc(1, sizeof(PageInfo) + (sizeof(BitmapCell) * PAGE_SIZE / (1<cell_size = cell_size; - info->num_cells = CELLS_OF_SIZE(cell_size); - info->num_free_cells = info->num_cells; - EJS_ASSERT(info->num_cells > 0); - info->page_start = page_data; - info->page_end = info->page_start + PAGE_SIZE; - // allocate a bitmap large enough to store any sized object so we can reuse the bitmap - info->page_bitmap = (BitmapCell*)(((char*)info) + sizeof(PageInfo)); - info->bump_ptr = info->page_start; - memset (info->page_bitmap, CELL_FREE, info->num_cells * sizeof(BitmapCell)); - return info; + for (int i = 0; i < root_registry_count; i++) + *root_registry[i] = _ejs_null; + free (root_registry); + root_registry = NULL; + root_registry_count = root_registry_capacity = 0; } -static PageInfo* -alloc_page_from_arena(Arena *arena, size_t cell_size) -{ - void *page_data = (void*)EJS_ALIGN(arena->pos, PAGE_SIZE); - if (arena->free_pages) { - PageInfo* info = arena->free_pages; - EJS_LIST_DETACH(info, arena->free_pages); - info->cell_size = cell_size; - info->num_cells = CELLS_OF_SIZE(cell_size); - info->num_free_cells = info->num_cells; - info->bump_ptr = info->page_start; - memset (info->page_bitmap, CELL_FREE, info->num_cells * sizeof(BitmapCell)); - SPEW(3, _ejs_log ("alloc_page_from_arena from free pages for cell size %zd = %p\n", info->cell_size, info)); - return info; - } - else if (page_data < arena->end) { - PageInfo* info = alloc_page_info_from_arena (arena, page_data, cell_size); - int page_idx = arena->num_pages++; - arena->pos = page_data + PAGE_SIZE; - arena->pages[page_idx] = page_data; - arena->page_infos[page_idx] = info; - SPEW(3, _ejs_log ("alloc_page_from_arena from bump pointer for cell size %zd = %p\n", info->cell_size, info)); - return info; - } - else { - return NULL; - } -} - -static int -compare_ptrs(const void* v1, const void* v2) -{ - Arena **a1 = (Arena**)v1; - Arena **a2 = (Arena**)v2; - ptrdiff_t diff = (intptr_t)*a1 - (intptr_t)*a2; - if (diff < 0) return -1; - if (diff == 0) return 0; - return 1; -} +// gc-P4: the compacting major (EJS_GC_COMPACT=off for A/B) and THE +// full-collection growth knob — a full GC triggers when old-gen growth +// since the last one exceeds gc_growth_pct percent of the post-sweep +// footprint (floor: two arenas, so small programs keep a sane cadence). +// The knob replaces the old fixed 60MB constant; with compaction +// shrinking the heap, the trigger now adapts in BOTH directions. +EJSBool compact_enabled; +static int gc_growth_pct = 50; -static Arena* -find_arena(GCObjectPtr ptr) +static size_t +full_gc_trigger(void) +{ + size_t t = heap_size_at_last_gc * (size_t)gc_growth_pct / 100; + size_t floor_ = 2 * (size_t)ARENA_SIZE; + return t > floor_ ? t : floor_; +} + +// ---- the collection policy ------------------------------------- +// +// Every collection the runtime initiates on its own behalf is decided +// HERE (GC.collect() and the shutdown collection are driver requests, +// not policy). Two inputs: old-gen growth since the last full +// collection — alloc_size - alloc_size_at_last_gc, promotions included +// — against full_gc_trigger(), and the EJS_GC_EVERY_N_ALLOC stress +// knob (minor cadence in nursery mode, full cadence in old mode). +// Each event preserves its historical baseline/counter resets exactly: +// AFTER_MINOR deliberately leaves num_allocs alone (the stress-minor +// cadence owns it), and ALLOC_FAILED collects even under +// EJS_GC_DISABLE — it is the allocator's last resort before throwing. +// (GCPolicyEvent lives in ejs-gc-internal.h; the minor collection +// reports AFTER_MINOR from its retirement path.) +void +gc_policy(GCPolicyEvent ev, const char* reason) { - Arena* arena_ptr = PTR_TO_ARENA(ptr); - - LOCK_ARENAS(); - // inlined bsearch - void* rv = NULL; - Arena**base = heap_arenas; - for (int lim = num_arenas; lim != 0; lim >>= 1) { - Arena** p = base + (lim >> 1); - ptrdiff_t diff = (intptr_t)arena_ptr - (intptr_t)*p; - if (diff == 0) { - rv = *p; - break; - } - if (diff > 0) { /* key > p: move right */ - base = p + 1; - lim--; - } /* else move left */ + if (ev == GC_POLICY_ALLOC_FAILED) { + _ejs_gc_collect (reason); + alloc_size_at_last_gc = alloc_size; + num_allocs = 0; + return; } - UNLOCK_ARENAS(); - if (!rv) return NULL; - return *(Arena**)rv; -} - -static Arena* -find_arena_in_array(GCObjectPtr ptr, Arena** array, int length) -{ - void* arena_ptr = PTR_TO_ARENA(ptr); - Arena **bsearch_rv = (Arena**)bsearch (&arena_ptr, array, length, sizeof(Arena*), compare_ptrs); - return bsearch_rv ? *bsearch_rv : NULL; -} - -static PageInfo* -find_page_and_cell_from_arena(GCObjectPtr ptr, uint32_t *cell_idx, Arena *arena) -{ - if (EJS_LIKELY (arena != NULL)) { - SANITY(verify_arena(arena)); - - int page_index = PTR_TO_ARENA_PAGE_INDEX(ptr); - - if (page_index < 0 || page_index > arena->num_pages) { - return NULL; - } - PageInfo *page = arena->page_infos[page_index]; + if (gc_disabled) + return; - if (!IS_ALIGNED_TO(ptr, page->cell_size)) { - return NULL; // can't possibly point to allocated cells. + switch (ev) { + case GC_POLICY_YOUNG_ALLOC: + if (collect_every_alloc && collect_every_alloc == num_allocs) { + num_allocs = 0; + _ejs_gc_minor_collect ("every_n_alloc"); } - - if (cell_idx) { - *cell_idx = PTR_TO_CELL(ptr, page); - EJS_ASSERT(*cell_idx >= 0 && *cell_idx < CELLS_IN_PAGE(page)); + break; + case GC_POLICY_OLD_ALLOC: + if (alloc_size - alloc_size_at_last_gc >= full_gc_trigger()) { + _ejs_gc_collect ("alloc_size"); + alloc_size_at_last_gc = alloc_size; + num_allocs = 0; } - - return page; - } - - // check if it's in the LOS - LOCK_GC(); - for (LargeObjectInfo *lobj = los_list; lobj; lobj = lobj->next) { - if (lobj->page_info.page_start == ptr) { - UNLOCK_GC(); - if (cell_idx) - *cell_idx = 0; - return &lobj->page_info; + else if (!nursery_enabled && collect_every_alloc && collect_every_alloc == num_allocs) { + _ejs_gc_collect ("every_n_alloc"); + alloc_size_at_last_gc = alloc_size; + num_allocs = 0; } - } - UNLOCK_GC(); - return NULL; -} - -static PageInfo* -find_page_and_cell(GCObjectPtr ptr, uint32_t *cell_idx) -{ - Arena* arena = find_arena_in_array(ptr, heap_arenas, num_arenas); - return find_page_and_cell_from_arena(ptr, cell_idx, arena); -} - -static void -set_gray (GCObjectPtr ptr) -{ - uint32_t cell_idx; - PageInfo *page = find_page_and_cell(ptr, &cell_idx); - if (!page) - return; - - SET_GRAY(page->page_bitmap[cell_idx]); -} - -static void -set_black (GCObjectPtr ptr) -{ - uint32_t cell_idx; - PageInfo *page = find_page_and_cell(ptr, &cell_idx); - if (!page) - return; - - SET_BLACK(page->page_bitmap[cell_idx]); -} - -static EJSBool -is_white (GCObjectPtr ptr) -{ - uint32_t cell_idx; - PageInfo *page = find_page_and_cell(ptr, &cell_idx); - if (!page) - return EJS_FALSE; - - return IS_WHITE(page->page_bitmap[cell_idx]); -} - -static PageInfo* -alloc_new_page(size_t cell_size) -{ - EJS_ASSERT(cell_size >= (1 << OBJECT_SIZE_LOW_LIMIT_BITS)); - SPEW(2, _ejs_log ("allocating new page for cell size %zd\n", cell_size)); - PageInfo *rv = NULL; - for (int i = 0; i < num_arenas; i ++) { - rv = alloc_page_from_arena(heap_arenas[i], cell_size); - if (rv) { - SPEW(2, _ejs_log (" => %p", rv)); - return rv; + break; + case GC_POLICY_AFTER_MINOR: + // when nearly every allocation is young, this is the only + // place the growth trigger can fire + if (alloc_size - alloc_size_at_last_gc >= full_gc_trigger()) { + _ejs_gc_collect ("promotion growth"); + alloc_size_at_last_gc = alloc_size; } + break; + case GC_POLICY_ALLOC_FAILED: // handled above + break; } - - // need a new arena - SPEW(2, _ejs_log ("unable to find page in current arenas, allocating a new one")); - LOCK_ARENAS(); - Arena* arena = arena_new(); - UNLOCK_ARENAS(); - if (arena == NULL) - return NULL; - rv = alloc_page_from_arena(arena, cell_size); - SPEW(2, _ejs_log (" => %p", rv)); - return rv; } -static void +void finalize_object(GCObjectPtr p) { GCObjectHeader* headerp = (GCObjectHeader*)p; @@ -626,11 +158,11 @@ finalize_object(GCObjectPtr p) } } -static void +void _ejs_finalize_obj(GCObjectPtr ptr, Arena* arena, PageInfo* info, uint32_t cell_idx) { EJS_ASSERT(info); - if (IS_FREE(info->page_bitmap[cell_idx])) { + if (cell_is_free(info->page_bitmap[cell_idx])) { return; } @@ -643,7 +175,7 @@ _ejs_finalize_obj(GCObjectPtr ptr, Arena* arena, PageInfo* info, uint32_t cell_i #endif info->cell_size); - SET_FREE(info->page_bitmap[cell_idx]); + cell_set_free(&info->page_bitmap[cell_idx]); SPEW(3, _ejs_log ("finalized object %p in page %p, num_free_cells == %zd\n", ptr, info, info->num_free_cells + 1)); // if this page is empty, move it to this arena's free list LOCK_PAGE(info); @@ -657,6 +189,14 @@ _ejs_finalize_obj(GCObjectPtr ptr, Arena* arena, PageInfo* info, uint32_t cell_i SPEW(2, _ejs_log ("releasing large object (size %zd)!\n", info->los_info->alloc_size)); release_to_los (info->los_info); } + else if (info->young) { + // a young survivor page emptied by a FULL sweep lives on + // heap_priv.young_pages, not a heap_pages bucket — + // detaching from the bucket list would silently unlink + // it from its young_pages neighbors while leaving that + // list's head/tail stale + young_page_freed (info, arena); + } else { EJS_ASSERT(arena); SPEW(2, _ejs_log ("page %p is empty, putting it on the free list\n", info)); @@ -680,13 +220,52 @@ _ejs_gc_init() if (n_allocs) collect_every_alloc = atoi(n_allocs); - // allocate an initial arenas - for (int i = 0; i < 10; i ++) - arena_new(); + // allocation/survival/pin instrumentation. The summary + // goes through atexit because _ejs_gc_shutdown is compiled out by + // default (GC_ON_SHUTDOWN in main.c). + gc_profile = getenv("EJS_GC_PROFILE") != NULL; + gettimeofday (&prof_start_tv, NULL); + if (gc_profile) + atexit (profile_report_shutdown); + + // the compacting major is the default; EJS_GC_COMPACT=off + // restores plain mark-sweep for A/B and differential runs + { + char* e = getenv("EJS_GC_COMPACT"); + compact_enabled = !(e && (strcmp(e, "off") == 0 || strcmp(e, "0") == 0)); + } + + // THE growth knob (gc-P4 knob census = 1): a full collection + // triggers when old-gen growth exceeds EJS_GC_GROWTH percent of the + // post-sweep footprint + { + char* growth = getenv("EJS_GC_GROWTH"); + if (growth) gc_growth_pct = atoi(growth); + if (gc_growth_pct <= 0) gc_growth_pct = 50; + } + + // the forwarding helpers are inert until the mover, so + // exercise them here on a scratch buffer when asked — a build whose + // header layout breaks the forwarding contract fails loudly instead + // of waiting for the collector to discover it. + if (getenv("EJS_GC_SELFTEST")) { + uint64_t scratch[2] = { EJS_SCAN_TYPE_OBJECT, 0 }; + uint64_t target[2] = { 0, 0 }; + EJS_ASSERT(!_ejs_gc_is_forwarded(&scratch)); + _ejs_gc_forward(&scratch, &target); + EJS_ASSERT(_ejs_gc_is_forwarded(&scratch)); + EJS_ASSERT(_ejs_gc_forwarding_addr(&scratch) == (GCObjectPtr)&target); + _ejs_log ("EJS_GC_SELFTEST: forwarding helpers ok\n"); + } + + // the arena reservation + initial arenas (ejs-gc-heap.c) + heap_space_init(); _ejs_gc_worklist_init(); - root_set = NULL; + // the generational nursery (EJS_GC_NURSERY=off selects + // the old single-generation collector for A/B and differential runs) + nursery_init(); } void @@ -699,494 +278,11 @@ _ejs_gc_allocate_oom_exceptions() page_allocation_failed_exc = _ejs_nativeerror_new_utf8 (EJS_ERROR, "page allocation failed"); } -static void -_scan_ejsvalue (ejsval val) -{ - if (!EJSVAL_IS_TRACEABLE_IMPL(val)) return; - - GCObjectPtr gcptr = (GCObjectPtr)EJSVAL_TO_GCTHING_IMPL(val); - - if (gcptr == NULL) return; - - WORKLIST_PUSH_AND_GRAY(gcptr); -} - -static void -_scan_from_ejsobject(EJSObject* obj) -{ - OP(obj,Scan)(obj, _scan_ejsvalue); -} - -static void -_scan_from_ejsprimstr(EJSPrimString *primStr) -{ - EJSPrimStringType strtype = EJS_PRIMSTR_GET_TYPE(primStr); - - switch (strtype) { - case EJS_STRING_ROPE: - // inline _scan_ejsvalue's push logic here to save creating an ejsval from the primStr only to destruct - // it in _scan_ejsvalue - - WORKLIST_PUSH_AND_GRAY(primStr->data.rope.left); - WORKLIST_PUSH_AND_GRAY(primStr->data.rope.right); - break; - case EJS_STRING_DEPENDENT: - WORKLIST_PUSH_AND_GRAY(primStr->data.dependent.dep); - break; - case EJS_STRING_FLAT: - // nothing to do here - break; - } -} - -static void -_scan_from_ejsprimsym(EJSPrimSymbol *primSymbol) -{ - _scan_ejsvalue (primSymbol->description); -} - -static void -_scan_from_ejsclosureenv(EJSClosureEnv *env) -{ - for (uint32_t i = 0; i < env->length; i ++) { - _scan_ejsvalue (env->slots[i]); - } -} - -static GCObjectPtr *stack_bottom; - -void -_ejs_gc_mark_thread_stack_bottom(GCObjectPtr* btm) -{ - stack_bottom = btm; -} - -static void -mark_pointers_in_range(GCObjectPtr* low, GCObjectPtr* high) -{ - GCObjectPtr* p; - for (p = low; p < high-1; p++) { - GCObjectPtr gcptr; - -#if OSX - // really a 64 bit check here, since for 64 bit systems, ejsvals can be stuck in registers, so we need to check if it's a valid - // ejsval gcthing as well. - ejsval ep = *(ejsval*)p; - if (EJSVAL_IS_GCTHING_IMPL(ep)) - gcptr = (GCObjectPtr)EJSVAL_TO_GCTHING_IMPL(ep); - else -#endif - gcptr = *p; - - if (gcptr == NULL) continue; // skip nulls. - - uint32_t cell_idx; - - PageInfo *page = find_page_and_cell(gcptr, &cell_idx); - if (!page) continue; // skip values outside our heap. - - // XXX more checks before we start treating the pointer like a GCObjectPtr? - BitmapCell cell = page->page_bitmap[cell_idx]; - if (IS_FREE(cell)) continue; // skip free cells - if (!IS_WHITE(cell)) continue; // skip pointers to gray/black cells - - WORKLIST_PUSH_AND_GRAY_CELL(gcptr, page->page_bitmap[cell_idx]); - } -} - -static void -mark_ejsvals_in_range(void* low, void* high) -{ - void* p = low; -#if IOS - while (((uintptr_t)p) & 0x7) { - p++; - } -#endif - for (; p < high - sizeof(ejsval); p += sizeof(ejsval)) { - ejsval candidate_val = *((ejsval*)p); - if (EJSVAL_IS_GCTHING_IMPL(candidate_val)) { - GCObjectPtr gcptr = (GCObjectPtr)EJSVAL_TO_GCTHING_IMPL(candidate_val); - - if (gcptr == NULL) continue; // skip nulls. - - uint32_t cell_idx; - PageInfo *page = find_page_and_cell(gcptr, &cell_idx); - if (page) { - // XXX more checks before we start treating the pointer like a GCObjectPtr? - BitmapCell cell = page->page_bitmap[cell_idx]; - if (IS_FREE(cell)) continue; // skip free cells - if (!IS_WHITE(cell)) continue; // skip pointers to gray/black cells - - if (EJSVAL_IS_STRING(candidate_val)) { - SPEW(4, _ejs_log ("found ptr to %p(PrimString) on stack\n", EJSVAL_TO_STRING(candidate_val))); - WORKLIST_PUSH_AND_GRAY_CELL(gcptr, page->page_bitmap[cell_idx]); - } - else { - //SPEW(_ejs_log ("found ptr to %p(%s) on stack\n", EJSVAL_TO_OBJECT(candidate_val), CLASSNAME(EJSVAL_TO_OBJECT(candidate_val)))); - WORKLIST_PUSH_AND_GRAY_CELL(gcptr, page->page_bitmap[cell_idx]); - } - } - } - } -} - -static int num_roots = 0; -static int white_objs = 0; -static int large_objs = 0; -static int total_objs = 0; - static int num_object_allocs = 0; static int num_closureenv_allocs = 0; static int num_primstr_allocs = 0; static int num_primsym_allocs = 0; - -static void -sweep_heap() -{ - int pages_visited = 0; - int pages_skipped = 0; - - // sweep the entire heap, freeing white nodes - for (int a = 0, e = num_arenas; a < e; a ++) { - Arena* arena = heap_arenas[a]; - - if (!arena) - continue; - - for (int p = 0, pe = arena->num_pages; p < pe; p++) { - PageInfo *info = arena->page_infos[p]; - - if (info->num_free_cells == info->num_cells) { - pages_skipped++; - } - else { - pages_visited ++; - - for (int c = 0, ce = info->num_cells; c < ce; c ++) { - BitmapCell cell = info->page_bitmap[c]; - - if (IS_FREE(cell)) - continue; - - total_objs++; - - if (IS_WHITE(cell)) { - white_objs++; - - GCObjectPtr gcobj = (GCObjectPtr)(info->page_start + c * info->cell_size); - _ejs_finalize_obj(gcobj, arena, info, c); - } - } - } - } - } - - // sweep the large object store - SPEW(2, _ejs_log ("sweeping los: ")); - LargeObjectInfo *lobj = los_list; - while (lobj) { - large_objs ++; - PageInfo *info = &lobj->page_info; - BitmapCell cell = info->page_bitmap[0]; - LargeObjectInfo *next = lobj->next; - if (IS_WHITE(cell)) { - // SPEW(2, { _ejs_log ("l"); fflush(stderr); }); - white_objs++; - - EJS_LIST_DETACH(lobj, los_list); - _ejs_finalize_obj(info->page_start, NULL, info, 0); - } - else { - // SPEW(2, { _ejs_log ("L"); fflush(stderr); }); - } - lobj = next; - } - SPEW(2, { _ejs_log ("\n"); }); -} - -static void -mark_from_roots() -{ - SPEW (2, _ejs_log ("marking from roots")); - - // mark from our registered roots - for (RootSetEntry *entry = root_set; entry; entry = entry->next) { - num_roots++; - if (entry->root) { - ejsval rootval = *entry->root; - if (!EJSVAL_IS_GCTHING_IMPL(rootval)) - continue; - GCObjectPtr root_ptr = (GCObjectPtr)EJSVAL_TO_GCTHING_IMPL(rootval); - if (root_ptr == NULL) - continue; - uint32_t cell_idx; - PageInfo* page = find_page_and_cell(root_ptr, &cell_idx); - if (!page) - continue; - - BitmapCell cell = page->page_bitmap[cell_idx]; - if (IS_FREE(cell)) continue; // skip free cells - if (!IS_WHITE(cell)) continue; // skip pointers to gray/black cells - WORKLIST_PUSH_AND_GRAY_CELL(root_ptr, page->page_bitmap[cell_idx]); - } - } - SPEW (2, _ejs_log ("done marking from roots")); -} - -static void -mark_from_modules() -{ - SPEW(2, _ejs_log ("marking from module exotics")); - - for (int i = 0; i < _ejs_num_modules; i ++) - _scan_from_ejsobject((EJSObject*)_ejs_modules[i]); -} - -#if TARGET_CPU_ARM -#define MARK_REGISTERS EJS_MACRO_START \ - GCObjectPtr __r0, __r1, __r2, __r3, __r4, __r5, __r6, __r7, __r8, __r9, __r10, __r11, __r12, __end; \ - __asm ("str r0, %0; str r1, %1; str r2, %2; str r3, %3; str r4, %4; str r5, %5; str r6, %6;" \ - "str r7, %7; str r8, %8; str r9, %9; str r10, %10; str r11, %11; str r12, %12;" \ - : "=m"(__r0), "=m"(__r1), "=m"(__r2), "=m"(__r3), "=m"(__r4), \ - "=m"(__r5), "=m"(__r6), "=m"(__r7), "=m"(__r8), "=m"(__r9), \ - "=m"(__r10), "=m"(__r11), "=m"(__r12)); \ - \ - mark_pointers_in_range(&__end, &__r0); \ - EJS_MACRO_END -#elif TARGET_CPU_AARCH64 -#define MARK_REGISTERS -#elif TARGET_CPU_AMD64 -#define MARK_REGISTERS EJS_MACRO_START \ - GCObjectPtr __rax, __rbx, __rcx, __rdx, __rsi, __rdi, __rbp, __rsp, __r8, __r9, __r10, __r11, __r12, __r13, __r14, __r15, __end; \ - __asm ("movq %%rax, %0; movq %%rbx, %1; movq %%rcx, %2; movq %%rdx, %3; movq %%rsi, %4;" \ - "movq %%rdi, %5; movq %%rbp, %6; movq %%rsp, %7; movq %%r8, %8; movq %%r9, %9;" \ - "movq %%r10, %10; movq %%r11, %11; movq %%r12, %12; movq %%r13, %13; movq %%r14, %14; movq %%r15, %15;" \ - : "=m"(__rax), "=m"(__rbx), "=m"(__rcx), "=m"(__rdx), "=m"(__rsi), \ - "=m"(__rdi), "=m"(__rbp), "=m"(__rsp), "=m"(__r8), "=m"(__r9), \ - "=m"(__r10), "=m"(__r11), "=m"(__r12), "=m"(__r13), "=m"(__r14), "=m"(__r15)); \ - \ - mark_pointers_in_range(&__end, &__rax); \ - EJS_MACRO_END -#elif TARGET_CPU_X86 -#define MARK_REGISTERS // just keep the build limping along -#else -#error "put code here to mark registers" -#endif - -static void -mark_thread_stack() -{ - MARK_REGISTERS; - - GCObjectPtr stack_top = NULL; - - mark_ejsvals_in_range(((void*)&stack_top) + sizeof(GCObjectPtr), stack_bottom); -} - -#define MAX_GENERATORS 256 -static int generator_count = 0; -static EJSGenerator* generators[MAX_GENERATORS]; - -void -_ejs_gc_push_generator(EJSGenerator* gen) -{ - generators[generator_count++] = gen; -} - -void -_ejs_gc_pop_generator() -{ - generator_count--; -} - -static void -mark_generator_stacks() -{ - for (int i = 0; i < generator_count; i++) { - EJSGenerator* gen = generators[i]; - - // XXX mark the actual stack - } -} - -static void -process_worklist() -{ - GCObjectPtr p; - while ((p = _ejs_gc_worklist_pop())) { - set_black (p); - GCObjectHeader* headerp = (GCObjectHeader*)p; - if ((*headerp & EJS_SCAN_TYPE_OBJECT) != 0) - _scan_from_ejsobject((EJSObject*)p); - else if ((*headerp & EJS_SCAN_TYPE_PRIMSTR) != 0) - _scan_from_ejsprimstr((EJSPrimString*)p); - else if ((*headerp & EJS_SCAN_TYPE_PRIMSYM) != 0) - _scan_from_ejsprimsym((EJSPrimSymbol*)p); - else if ((*headerp & EJS_SCAN_TYPE_CLOSUREENV) != 0) - _scan_from_ejsclosureenv((EJSClosureEnv*)p); - } - - EJS_ASSERT(work_list.list == NULL); -} - -static void -_ejs_gc_collect_inner(EJSBool shutting_down) -{ -#if gc_timings > 1 - struct timeval tvbefore, tvafter; -#endif - - // very simple stop the world collector - SPEW(1, _ejs_log ("collection started\n")); - - num_roots = 0; - white_objs = 0; - large_objs = 0; - total_objs = 0; - -#if gc_timings > 1 - gettimeofday (&tvbefore, NULL); -#endif - - if (!shutting_down) { - mark_from_roots(); - - total_objs = num_roots; - - mark_from_modules(); - - mark_thread_stack(); - - mark_generator_stacks(); - - process_worklist(); - } - -#if gc_timings > 1 - gettimeofday (&tvafter, NULL); -#endif - -#if gc_timings > 1 - { - uint64_t usec_before = tvbefore.tv_sec * 1000000 + tvbefore.tv_usec; - uint64_t usec_after = tvafter.tv_sec * 1000000 + tvafter.tv_usec; - - _ejs_log ("gc scan took %gms\n", (usec_after - usec_before) / 1000.0); - } -#endif - -#if gc_timings > 1 - gettimeofday (&tvbefore, NULL); -#endif - - sweep_heap(); - -#if gc_timings > 1 - { - gettimeofday (&tvafter, NULL); - } -#endif - -#if gc_timings > 1 - { - uint64_t usec_before = tvbefore.tv_sec * 1000000 + tvbefore.tv_usec; - uint64_t usec_after = tvafter.tv_sec * 1000000 + tvafter.tv_usec; - - _ejs_log ("gc sweep took %gms\n", (usec_after - usec_before) / 1000.0); - } -#endif - -#if gc_timings > 1 - _ejs_log ("_ejs_gc_collect stats:\n"); - _ejs_log (" num_roots: %d\n", num_roots); - _ejs_log (" total objects: %d\n", total_objs); - _ejs_log (" num large objects: %d\n", large_objs); - _ejs_log (" garbage objects: %d\n", white_objs); -#endif - - unsigned int tmp = black_mask; - black_mask = white_mask; - white_mask = tmp; - - if (shutting_down) { - // NULL out all of our roots - - RootSetEntry *entry = root_set; - while (entry) { - RootSetEntry *next = entry->next; - *entry->root = _ejs_null; - free (entry); - entry = next; - } - - root_set = NULL; - - SPEW(1, _ejs_log ("final gc page statistics:\n"); - for (int hp = 0; hp < HEAP_PAGELISTS_COUNT; hp++) { - int len = 0; - - EJS_LIST_FOREACH (&heap_pages[hp], PageInfo, page, { - len ++; - }); - - _ejs_log (" size: %d pages: %d\n", 1<<(hp + 3), len); - }); - } -#if sanity - else { - for (int hp = 0; hp < HEAP_PAGELISTS_COUNT; hp++) { - EJS_LIST_FOREACH (&heap_pages[hp], PageInfo, page, { - for (int c = 0; c < CELLS_IN_PAGE (page); c ++) { - if (!IS_FREE(page->page_bitmap[c]) && !IS_WHITE(page->page_bitmap[c])) - continue; - } - }) - } - } -#endif - SPEW(1, _ejs_log ("collection finished\n")); -} - -static size_t -calc_heap_size() -{ - size_t size = 0; - for (int hp = 0; hp < HEAP_PAGELISTS_COUNT; hp++) { - size += _ejs_list_length(&heap_pages[hp]) * PAGE_SIZE; - } - return size; -} - -void -_ejs_gc_collect(const char *reason) -{ - SPEW(1, _ejs_log ("_ejs_gc_collect(%s)\n", reason)); -#if gc_timings > 0 - struct timeval tvbefore, tvafter; - - gettimeofday (&tvbefore, NULL); - - int heap_size = calc_heap_size(); -#endif - - _ejs_gc_collect_inner(EJS_FALSE); - -#if gc_timings > 0 - gettimeofday (&tvafter, NULL); - - uint64_t usec_before = tvbefore.tv_sec * 1000000 + tvbefore.tv_usec; - uint64_t usec_after = tvafter.tv_sec * 1000000 + tvafter.tv_usec; - - _ejs_log ("gc collect took %gms\n", (usec_after - usec_before) / 1000.0); - _ejs_log (" for a heap size of %zdMB\n", heap_size/(1024*1024)); -#if gc_timings > 1 - _ejs_gc_dump_heap_stats(); -#endif -#endif -} - int total_allocs = 0; void @@ -1195,6 +291,9 @@ _ejs_gc_shutdown() _ejs_gc_collect_inner(EJS_TRUE); SPEW(1, _ejs_log ("total allocs = %d\n", total_allocs)); + if (gc_profile) + profile_report_shutdown(); + _ejs_log ("gc allocation stats (_ejs_gc_shutdown):\n"); _ejs_log (" objects: %d\n", num_object_allocs); _ejs_log (" closureenv: %d\n", num_closureenv_allocs); @@ -1220,86 +319,6 @@ pow2_ceil(size_t x) return (x); } -static GCObjectPtr -alloc_from_page(PageInfo *info) -{ - LOCK_PAGE(info); - - EJS_ASSERT (info->num_free_cells > 0); - - GCObjectPtr rv = NULL; - uint32_t cell; - - SPEW(2, _ejs_log ("allocating object from page %p (cell size %zd)\n", info, info->cell_size)); - - if (info->bump_ptr) { - rv = (GCObjectPtr)EJS_ALIGN(info->bump_ptr, 8); - cell = PTR_TO_CELL(info->bump_ptr, info); - info->bump_ptr += info->cell_size; - // check if we can service the next alloc request from the bump_ptr. if we can't, switch - // to the freelist code below. - if (info->bump_ptr + info->cell_size >= info->page_end) - info->bump_ptr = NULL; - } - else { - for (cell = 0; cell < info->num_cells; cell ++) { - if (IS_FREE(info->page_bitmap[cell])) { - rv = info->page_start + (cell * info->cell_size); - break; - } - } - } - - EJS_ASSERT (rv); - - SET_ALLOCATED(info->page_bitmap[cell]); - SET_WHITE(info->page_bitmap[cell]); - - info->num_free_cells --; - - UNLOCK_PAGE(info); - - SPEW(2, _ejs_log ("allocated obj %p from page %p (cell size %zd), free cells remaining %zd\n", rv, info, info->cell_size, info->num_free_cells)); - -#if !clear_on_finalize - memset(rv, 0, info->cell_size); -#endif - return rv; -} - -static GCObjectPtr -alloc_from_los(size_t size, EJSScanType scan_type) -{ - // allocate enough space for the object, our header, and our bitmap. leave room enough to align the return value - LargeObjectInfo *rv = alloc_from_os(size + sizeof(LargeObjectInfo) + 16, 0); - if (rv == NULL) - return NULL; - - rv->page_info.page_bitmap = (char*)((void*)rv + sizeof(LargeObjectInfo)); // our bitmap comes right after the header - rv->page_info.page_start = (void*)EJS_ALIGN((void*)rv + sizeof(LargeObjectInfo) + 8, 8); - rv->page_info.cell_size = size; - rv->page_info.num_cells = 1; - rv->page_info.num_free_cells = 0; - rv->page_info.los_info = rv; - - SET_WHITE(rv->page_info.page_bitmap[0]); - SET_ALLOCATED(rv->page_info.page_bitmap[0]); - - *((GCObjectHeader*)rv->page_info.page_start) = scan_type; - - rv->alloc_size = size; - - EJS_LIST_PREPEND (rv, los_list); - //_ejs_log ("alloc_from_los returning %p\n, los_list = %p\n", rv->page_info.page_start, los_list); - return rv->page_info.page_start; -} - -static void -release_to_los (LargeObjectInfo *lobj) -{ - release_to_os (lobj, lobj->alloc_size); -} - size_t alloc_size = 0; int num_allocs = 0; size_t alloc_size_at_last_gc = 0; @@ -1309,8 +328,6 @@ _ejs_gc_alloc(size_t size, EJSScanType scan_type) { GCObjectPtr rv = NULL; - alloc_size += size; - num_allocs ++; total_allocs ++; @@ -1321,30 +338,55 @@ _ejs_gc_alloc(size_t size, EJSScanType scan_type) case EJS_SCAN_TYPE_CLOSUREENV: num_closureenv_allocs ++; break; } - if (!gc_disabled) { - char *gc_reason = NULL; - if (alloc_size - alloc_size_at_last_gc >= 60 * 1024 * 1024) { - gc_reason = "alloc_size"; - } else if (collect_every_alloc && collect_every_alloc == num_allocs) { - gc_reason = "every_n_alloc"; + int bucket; + int bucket_size = MAX(pow2_ceil(size), 1< 0 + void* p = _ejs_heap.bump[idx]; + if (EJS_LIKELY((char*)p + bucket_size <= (char*)_ejs_heap.limit[idx])) { + _ejs_heap.bump[idx] = (char*)p + bucket_size; + memset (p, 0, bucket_size); + *(GCObjectHeader*)p = scan_type | EJS_GC_HEADER_YOUNG; + gc_watch_hit ("young-alloc-fast", p); + return p; } + rv = young_alloc_slow(idx, bucket_size, scan_type); + if (rv) { gc_watch_hit ("young-alloc-slow", rv); return rv; } + // nursery unusable (pathologically pinned): fall through to the + // old allocator } - int bucket; - int bucket_size = MAX(pow2_ceil(size), 1< OBJECT_SIZE_HIGH_LIMIT_BITS) { + if (bucket > OBJECT_SIZE_HIGH_LIMIT_BITS + 1) { SPEW(2, _ejs_log ("need to alloc %zd from los!!!\n", size)); rv = alloc_from_los(size, scan_type); + if (rv && nursery_enabled) { + // LOS objects are old at birth: their construction stores + // bypass the barrier, so they start DIRTY and get a precise + // scan at the next minor + _ejs_gc_remember_slow(rv); + } if (rv == NULL) { if (num_allocs == 0) { _ejs_log ("los allocation (size = %d) failed twice, throwing", size); @@ -1353,9 +395,7 @@ _ejs_gc_alloc(size_t size, EJSScanType scan_type) else { _ejs_log ("los allocation (size = %d) failed, trying to collect", size); UNLOCK_GC(); - _ejs_gc_collect ("los allocation fail"); - alloc_size_at_last_gc = alloc_size; - num_allocs = 0; + gc_policy (GC_POLICY_ALLOC_FAILED, "los allocation fail"); goto retry_allocation; } } @@ -1376,9 +416,7 @@ _ejs_gc_alloc(size_t size, EJSScanType scan_type) else { _ejs_log ("page allocation failed, trying to collect"); UNLOCK_GC(); - _ejs_gc_collect ("page allocation fail"); - alloc_size_at_last_gc = alloc_size; - num_allocs = 0; + gc_policy (GC_POLICY_ALLOC_FAILED, "page allocation fail"); goto retry_allocation; } } @@ -1386,7 +424,12 @@ _ejs_gc_alloc(size_t size, EJSScanType scan_type) } rv = alloc_from_page(info); - *((GCObjectHeader*)rv) = scan_type; + // zero the cell: recycled cells are filled with 0xaf on finalize, and a + // collection can scan this object before its constructor initializes it + // (any allocation between _ejs_gc_alloc and _ejs_init_object can + // trigger one). zeroed contents are inert to the scanner. + memset (rv, 0, info->cell_size); + *((GCObjectHeader*)rv) = scan_type | EJS_GC_HEADER_YOUNG; if (info->num_free_cells == 0) { // if the page is full, bump it to the end of the list (if there's more than 1 page in the list) @@ -1404,89 +447,47 @@ _ejs_gc_alloc(size_t size, EJSScanType scan_type) void _ejs_gc_add_root(ejsval* root) { - RootSetEntry* entry = (RootSetEntry*)malloc(sizeof(RootSetEntry)); - EJS_LIST_INIT(entry); - entry->root = root; - EJS_LIST_PREPEND(entry, root_set); + if (root_registry_count == root_registry_capacity) { + root_registry_capacity = root_registry_capacity ? root_registry_capacity * 2 : 512; + root_registry = realloc (root_registry, root_registry_capacity * sizeof(ejsval*)); + } + root_registry[root_registry_count++] = root; } void _ejs_gc_remove_root(ejsval* root) { - RootSetEntry *entry = NULL; - - for (entry = root_set; entry; entry = entry->next) { - if (entry->root == root) { - EJS_LIST_DETACH(entry, root_set); - free (entry); + for (int i = 0; i < root_registry_count; i++) { + if (root_registry[i] == root) { + root_registry[i] = root_registry[--root_registry_count]; return; } } } +// object-remembering barrier: mark `owner` dirty and queue it for +// the next minor's rescan. The inline half (ejs-gc.h) already filtered +// non-young values, young owners, and already-dirty owners. void -_ejs_gc_mark_conservative_range(void* low, void* high) { - mark_ejsvals_in_range(low, high); -} - -static int -page_list_count (PageInfo* page) +_ejs_gc_remember_slow(void* owner) { - int count = 0; - while (page) { - count ++; - page = page->next; - } - return count; + GCObjectHeader* h = (GCObjectHeader*)owner; + *h |= EJS_GC_HEADER_DIRTY; + EJSHeapContext* c = &_ejs_heap; + if (EJS_LIKELY(c->remset_count < c->remset_capacity)) + c->remset[c->remset_count++] = owner; + else + c->remset_overflowed = 1; } +// the emitted barrier's out-of-line half: emit.ts inlines only the +// value-is-young range check (double payloads may false-positive; the +// full filter reruns here) void -_ejs_gc_dump_heap_stats() +_ejs_gc_remember_val(ejsval owner, ejsval val) { - _ejs_log ("arenas:\n"); - for (int i = 0; i < num_arenas; i ++) { - _ejs_log (" [%d] - %p - %p\n", i, heap_arenas[i], heap_arenas[i]->end); - } - - for (int i = 0; i < HEAP_PAGELISTS_COUNT; i ++) { -#if gc_timings > 3 - EJSBool printed_something = EJS_FALSE; -#endif - _ejs_log ("heap_pages[%d, size %d] : %d pages\n", i, 1 << (i + OBJECT_SIZE_LOW_LIMIT_BITS), _ejs_list_length (&heap_pages[i])); -#if gc_timings > 3 - EJS_LIST_FOREACH (&heap_pages[i], PageInfo, page, { - GCObjectPtr p = page->page_start; - for (int c = 0; c < CELLS_IN_PAGE (page); c ++, p += page->cell_size) { - if (IS_FREE(page->page_bitmap[c])) - continue; - GCObjectHeader* headerp = (GCObjectHeader*)p; - if ((*headerp & EJS_SCAN_TYPE_OBJECT) != 0) _ejs_log ("O"); - else if ((*headerp & EJS_SCAN_TYPE_CLOSUREENV) != 0) _ejs_log ("C"); - else if ((*headerp & EJS_SCAN_TYPE_PRIMSTR) != 0) _ejs_log (((*headerp >> EJS_GC_USER_FLAGS_SHIFT) & 0x10) != 0 ? "s" : "S"); - else if ((*headerp & EJS_SCAN_TYPE_PRIMSYM) != 0) _ejs_log ("X"); - printed_something = EJS_TRUE; - } - }) - if (printed_something) - _ejs_log ("\n"); -#endif - } - - _ejs_log ("\n"); - -#if spew >= 2 - if (los_list) { - _ejs_log ("large object store: "); - for (LargeObjectInfo* lobj = los_list; lobj; lobj = lobj->next) { - GCObjectHeader* headerp = (GCObjectHeader*)lobj->page_info.page_start; - if ((*headerp & EJS_SCAN_TYPE_OBJECT) != 0) _ejs_log ("O"); - else if ((*headerp & EJS_SCAN_TYPE_CLOSUREENV) != 0) _ejs_log ("C"); - else if ((*headerp & EJS_SCAN_TYPE_PRIMSTR) != 0) _ejs_log ("S"); - else if ((*headerp & EJS_SCAN_TYPE_PRIMSYM) != 0) _ejs_log ("X"); - } - _ejs_log ("\n"); - } -#endif + void* p = (void*)EJSVAL_TO_GCTHING_IMPL(owner); + if (p) _ejs_gc_remember(p, val); } ///////// @@ -1497,6 +498,12 @@ static EJS_NATIVE_FUNC(_ejs_GC_collect) { return _ejs_undefined; } +// committed old-gen page bytes (the compaction gate's observable: +// this number DROPS when the heap shrinks) +static EJS_NATIVE_FUNC(_ejs_GC_heapSize) { + return NUMBER_TO_EJSVAL((double)calc_heap_size()); +} + static EJS_NATIVE_FUNC(_ejs_GC_dumpAllocationStats) { char* tag = NULL; @@ -1530,7 +537,7 @@ static EJS_NATIVE_FUNC(_ejs_GC_dumpLiveStrings) { EJS_LIST_FOREACH (&heap_pages[i], PageInfo, page, { GCObjectPtr p = page->page_start; for (int c = 0; c < CELLS_IN_PAGE (page); c ++, p += page->cell_size) { - if (IS_FREE(page->page_bitmap[c])) + if (cell_is_free(page->page_bitmap[c])) continue; GCObjectHeader* headerp = (GCObjectHeader*)p; @@ -1581,6 +588,7 @@ _ejs_GC_init(ejsval ejs_obj) #define OBJ_METHOD(x) EJS_INSTALL_ATOM_FUNCTION(_ejs_GC, x, _ejs_GC_##x) OBJ_METHOD(collect); + OBJ_METHOD(heapSize); OBJ_METHOD(dumpAllocationStats); OBJ_METHOD(dumpLiveStrings); diff --git a/runtime/ejs-gc.h b/runtime/ejs-gc.h index 9038c1ac..d806d5cb 100644 --- a/runtime/ejs-gc.h +++ b/runtime/ejs-gc.h @@ -41,6 +41,157 @@ extern GCObjectPtr _ejs_gc_alloc(size_t size, EJSScanType scan_type); #define _ejs_gc_new_closureenv(sz) \ (EJSClosureEnv *)_ejs_gc_alloc(sz, EJS_SCAN_TYPE_CLOSUREENV) +// ---- forwarding plumbing --------------------------------------- +// +// Inert until a mover (minor evacuation / major compaction) consumes it; +// landed now so the header bit inventory is complete and the helpers are +// exercised (EJS_GC_SELFTEST=1) with the old collector still active. +// +// Forwarding uses the classic first-word overwrite: once an object has been +// evacuated its old header is dead (the copy carries the real one), so the +// old slot's header word becomes the forwarding record — the target address +// in the low bits (heap addresses live below 2^47 by the NaN-boxing rule) +// plus a discriminator bit chosen ABOVE the address range from the header's +// gc-reserved bits (57-63; see ejs-types.h). A live header can never be +// mistaken for a forwarding record (bit 59 is written by nothing else), and +// a forwarding record can never be mistaken for a live header of any scan +// type worth trusting — readers must check _ejs_gc_is_forwarded first, as +// the evacuation loop will. +#define EJS_GC_HEADER_FORWARDED (1ULL << 59) +#define EJS_GC_FORWARD_ADDR_MASK ((1ULL << 47) - 1) + +typedef uint64_t GCObjectHeaderWord; // matches GCObjectHeader (ejs-types.h) + +static inline EJSBool +_ejs_gc_is_forwarded(GCObjectPtr p) +{ + return (*(GCObjectHeaderWord*)p & EJS_GC_HEADER_FORWARDED) != 0; +} + +static inline GCObjectPtr +_ejs_gc_forwarding_addr(GCObjectPtr p) +{ + return (GCObjectPtr)(uintptr_t)(*(GCObjectHeaderWord*)p & EJS_GC_FORWARD_ADDR_MASK); +} + +// overwrite `from`'s header with a forwarding record pointing at `to`. +// `to` must be 8-aligned and below 2^47 (both invariants of the allocator). +static inline void +_ejs_gc_forward(GCObjectPtr from, GCObjectPtr to) +{ + *(GCObjectHeaderWord*)from = + ((GCObjectHeaderWord)(uintptr_t)to & EJS_GC_FORWARD_ADDR_MASK) + | EJS_GC_HEADER_FORWARDED; +} + +// ---- the heap context + generational write barrier ------------- +// +// ALL new collector state lives in the heap context (the Concurrency-II +// discipline: an isolate is "one more context", never "another pile of +// file statics"). The leading fields are THE emitted-code seam — the +// emitter reads bump/limit/nursery bounds through this struct's +// exported symbol, so their order and offsets are part of the emitter +// contract: append, never reorder. +// +// The nursery is one dedicated arena, so "is young" is a raw range +// check — cheap enough for the inline write barrier and the emitted +// fast paths. With the nursery disabled (EJS_GC_NURSERY=off) the +// bounds are NULL and every check below degrades to a no-op / the +// old allocator path. + +#define EJS_GC_NUM_SIZE_CLASSES 5 // ffs buckets: 16/32/64/128/256 cells + +typedef struct { + // -- emitted-code seam (offsets fixed; append only) -- + void* bump[EJS_GC_NUM_SIZE_CLASSES]; // current young page cursor, per class + void* limit[EJS_GC_NUM_SIZE_CLASSES]; // current young page end, per class + void* nursery_base; // [base, end) = the nursery arena + void* nursery_end; + // -- the dirty-OBJECT buffer (object-remembering): OLD objects + // whose owned storage received a YOUNG reference; deduped by the + // DIRTY header bit. (The future concurrent-marking SATB log rides the same + // structure.) -- + void** remset; + int32_t remset_count; + int32_t remset_capacity; + int32_t remset_overflowed; // fall back to a full old-gen scan this minor + // the top of the CURRENT machine stack (main stack bottom, or the + // running generator's stack end) — maintained by the generator + // push/pop hooks so the barrier can reject transient stack slots + void* current_stack_end; + // -- runtime-private state (an opaque struct in ejs-gc.c) -- + void* priv; + // -- head of the CURRENT stack's gc-frame chain (word 17 + // of the emitted seam). Emitted prologues link an EJSGCFrame + // here, epilogues unlink, catch handlers re-link their own frame + // (unwound callees' records die with their stack). Each machine + // stack owns a disjoint chain: the generator push/pop hooks swap + // this head alongside current_stack_end, and suspended + // generators' chains are walked via their saved heads. Minor + // collections process every chain slot PRECISELY (evacuate + + // rewrite) BEFORE the conservative pin pass — a frame-held young + // object therefore MOVES every minor, and the conservative + // scanner's stale copies of it skip via the forwarding check. + void* gc_frame_head; +} EJSHeapContext; + +// an emitted function's precise-root record, alloca'd in +// its own frame. `slots` hold BOXED ejsvals only (raw f64/i1 values +// are invisible to GC by construction); the emitter initializes every +// slot to undefined at entry — a stale slot must still parse as a +// valid ejsval, never as stack garbage. +typedef struct _EJSGCFrame { + struct _EJSGCFrame* prev; + uintptr_t count; + ejsval slots[1]; // really `count` of them +} EJSGCFrame; + +extern EJSHeapContext _ejs_heap; + +static inline EJSBool +_ejs_gc_is_young(void* p) +{ + return (char*)p >= (char*)_ejs_heap.nursery_base + && (char*)p < (char*)_ejs_heap.nursery_end; +} + +// The generational write barrier — OBJECT-REMEMBERING (the second +// design). The first design recorded raw slot addresses; slots inside +// malloc'd satellites (element buffers, descriptors, map entries) kept +// dangling into freed memory — a structural hazard, not a bug tail. +// This design records the OWNING heap object instead: the minor rescans +// a dirty object through its Scan specop, which walks whatever storage +// the object owns AT SCAN TIME. No captured interior pointers, no +// lifetime coupling. Dedup is the DIRTY header bit; the buffer gets +// each old object at most once per cycle. +// +// Contract: after storing a traceable value anywhere in `owner`'s +// transitive OWNED storage (inline slots, element vector, property map, +// descriptors), call _ejs_gc_remember(owner_ptr, value). Young owners +// and non-young values filter out. +#define EJS_GC_HEADER_DIRTY (1ULL << 60) + +extern void _ejs_gc_remember_slow(void* owner); + +static inline void +_ejs_gc_remember(void* owner, ejsval newval) +{ + if (!EJSVAL_IS_TRACEABLE_IMPL(newval)) return; + void* target = (void*)EJSVAL_TO_GCTHING_IMPL(newval); + if (!_ejs_gc_is_young(target)) return; + if (_ejs_gc_is_young(owner)) return; + GCObjectHeaderWord* h = (GCObjectHeaderWord*)owner; + if (*h & EJS_GC_HEADER_DIRTY) return; + _ejs_gc_remember_slow(owner); +} + +// object-flavored convenience (most call sites hold the ejsval) +#define EJS_GC_REMEMBER(ownerval, v) \ + _ejs_gc_remember((void*)EJSVAL_TO_OBJECT_IMPL(ownerval), (v)) + +// object-flavored emitted entry (emit.ts passes the owner ejsval) +extern void _ejs_gc_remember_val(ejsval owner, ejsval val); + extern void _ejs_gc_add_root(ejsval *val); extern void _ejs_gc_remove_root(ejsval *root); diff --git a/runtime/ejs-generator.c b/runtime/ejs-generator.c index be8a4d7d..ad218b25 100644 --- a/runtime/ejs-generator.c +++ b/runtime/ejs-generator.c @@ -65,7 +65,7 @@ static void _ejs_iterator_wrapper_specop_scan (EJSObject* obj, EJSValueFunc scan_func) { EJSIteratorWrapper* iter = (EJSIteratorWrapper*)obj; - scan_func(iter->iterator); + scan_func(&(iter->iterator)); _ejs_Object_specops.Scan (obj, scan_func); } @@ -114,17 +114,47 @@ _ejs_iterator_wrapper_new (ejsval iterator) return OBJECT_TO_EJSVAL(rv); } -#define GENERATOR_STACK_SIZE 64 * 1024 +#define GENERATOR_STACK_SIZE 512 * 1024 static void _ejs_generator_start(EJSGenerator* gen) { _ejs_gc_push_generator(gen); ejsval undef_this = _ejs_undefined; - _ejs_invoke_closure(gen->body, &undef_this, 0, NULL, _ejs_undefined); + // catch here: an uncaught throw out of the body must not unwind the + // generator stack past this frame (there is nothing above it but the + // makecontext trampoline). The exception is parked in yielded_value + // and rethrown by the resume site on the caller's stack. + ejsval rv; + EJSBool body_returned = _ejs_invoke_closure_catch(&rv, gen->body, &undef_this, 0, NULL, _ejs_undefined); + + // the body's return value is the final iteration result's value + // (`function* g() { return 5; }` -> { value: 5, done: true }). + // The iter result is allocated BEFORE the generator leaves the active + // chain: we are still executing on the generator's stack here, and a + // collection triggered by this allocation must know that (found the hard way — + // mark_thread_stack's range depends on the chain). + gen->completed = EJS_TRUE; + if (body_returned) { + gen->yielded_value = _ejs_create_iter_result(rv, _ejs_true); + } + else { + gen->threw_out = EJS_TRUE; + gen->yielded_value = rv; + } + _ejs_gc_remember(gen, gen->yielded_value); _ejs_gc_pop_generator(); +} - gen->yielded_value = _ejs_create_iter_result(_ejs_undefined, _ejs_true); +// makecontext's variadic arguments are ints, so a 64-bit pointer passed +// directly gets truncated (which is how generators crashed on arm64 +// macos: heap pointers there don't fit in 32 bits). split the pointer +// across two int args, posix-style. +static void +_ejs_generator_trampoline(unsigned int gen_lo, unsigned int gen_hi) +{ + EJSGenerator* gen = (EJSGenerator*)(((uint64_t)gen_hi << 32) | gen_lo); + _ejs_generator_start(gen); } ejsval @@ -135,15 +165,29 @@ _ejs_generator_new (ejsval generator_body) rv->body = generator_body; rv->started = EJS_FALSE; + rv->completed = EJS_FALSE; + rv->threw_out = EJS_FALSE; + rv->throwing = EJS_FALSE; + rv->returning = EJS_FALSE; rv->yielded_value = _ejs_undefined; rv->sent_value = _ejs_undefined; rv->stack = malloc(GENERATOR_STACK_SIZE); + rv->stack_size = GENERATOR_STACK_SIZE; + rv->caller_stack_top = NULL; + rv->gc_frame_head = NULL; // this stack's parked chain + rv->caller_gc_frame_head = NULL; + rv->reg_prev = NULL; + rv->reg_next = _ejs_generator_registry; + if (_ejs_generator_registry) _ejs_generator_registry->reg_prev = rv; + _ejs_generator_registry = rv; getcontext(&rv->generator_context); rv->generator_context.uc_stack.ss_sp = rv->stack; rv->generator_context.uc_stack.ss_size = GENERATOR_STACK_SIZE; rv->generator_context.uc_link = &rv->caller_context; - makecontext(&rv->generator_context, (void(*)(void))_ejs_generator_start, 1, rv); + makecontext(&rv->generator_context, (void(*)(void))_ejs_generator_trampoline, 2, + (unsigned int)(uint64_t)(uintptr_t)rv, + (unsigned int)(((uint64_t)(uintptr_t)rv) >> 32)); memset(&rv->caller_context, 0, sizeof(rv->caller_context)); return OBJECT_TO_EJSVAL(rv); @@ -153,6 +197,7 @@ ejsval _ejs_generator_yield (ejsval generator, ejsval arg) { EJSGenerator* gen = (EJSGenerator*)EJSVAL_TO_OBJECT(generator); gen->yielded_value = _ejs_create_iter_result(arg, _ejs_false); + _ejs_gc_remember(gen, gen->yielded_value); gen->sent_value = _ejs_undefined; _ejs_gc_pop_generator(); @@ -164,16 +209,41 @@ _ejs_generator_yield (ejsval generator, ejsval arg) { _ejs_throw (gen->sent_value); } + if (gen->returning) { + gen->returning = EJS_FALSE; + // unwind the generator body: finally blocks run; the desugared + // body's outer catch recognizes the sentinel and returns + // gen->sent_value (see DesugarGeneratorFunctions) + _ejs_throw (_ejs_generator_return_sentinel); + } + return gen->sent_value; } +// every swap back from the generator lands here: if the body ended in +// an uncaught throw, rethrow it now — on the caller's stack +static ejsval +_ejs_generator_resume_result (EJSGenerator* gen) +{ + if (gen->threw_out) { + gen->threw_out = EJS_FALSE; + ejsval exc = gen->yielded_value; + gen->yielded_value = _ejs_undefined; + _ejs_throw (exc); + } + return gen->yielded_value; +} + static ejsval _ejs_generator_send (ejsval generator, ejsval arg) { EJSGenerator* gen = (EJSGenerator*)EJSVAL_TO_OBJECT(generator); + gen->started = EJS_TRUE; gen->yielded_value = _ejs_undefined; gen->sent_value = arg; + _ejs_gc_remember(gen, gen->sent_value); + gen->caller_stack_top = (void*)&gen; // GC: the suspended segment starts here swapcontext(&gen->caller_context, &gen->generator_context); - return gen->yielded_value; + return _ejs_generator_resume_result(gen); } static ejsval @@ -182,8 +252,27 @@ _ejs_generator_throw (ejsval generator, ejsval arg) { gen->yielded_value = _ejs_undefined; gen->sent_value = arg; gen->throwing = EJS_TRUE; + gen->caller_stack_top = (void*)&gen; // GC: the suspended segment starts here swapcontext(&gen->caller_context, &gen->generator_context); - return gen->yielded_value; + return _ejs_generator_resume_result(gen); +} + +// the unforgeable value .return() throws through the generator body to +// unwind it (running finally blocks); the desugared body's outermost +// catch converts it into a normal return +ejsval _ejs_generator_return_sentinel EJSVAL_ALIGNMENT; + +ejsval +_ejs_generator_is_return_sentinel (ejsval exc) +{ + return BOOLEAN_TO_EJSVAL(EJSVAL_EQ(exc, _ejs_generator_return_sentinel)); +} + +ejsval +_ejs_generator_return_value (ejsval generator) +{ + EJSGenerator* gen = (EJSGenerator*)EJSVAL_TO_OBJECT(generator); + return gen->sent_value; } static EJS_NATIVE_FUNC(_ejs_Generator_prototype_throw) { @@ -194,12 +283,42 @@ static EJS_NATIVE_FUNC(_ejs_Generator_prototype_throw) { if (!EJSVAL_IS_GENERATOR(O)) _ejs_throw_nativeerror_utf8(EJS_TYPE_ERROR, ".throw called on non-generator"); + EJSGenerator* gen = (EJSGenerator*)EJSVAL_TO_OBJECT(O); + // 25.3.3.4: throwing at a completed (or never-started) generator + // just throws the exception in the caller + if (gen->completed || !gen->started) + _ejs_throw (argc > 0 ? args[0] : _ejs_undefined); + return _ejs_generator_throw(O, argc > 0 ? args[0] : _ejs_undefined); } static EJS_NATIVE_FUNC(_ejs_Generator_prototype_return) { - printf ("generator .return not implemented\n"); - abort(); + ejsval O = *_this; + if (!EJSVAL_IS_OBJECT(O)) + _ejs_throw_nativeerror_utf8(EJS_TYPE_ERROR, ".return called on non-object"); + + if (!EJSVAL_IS_GENERATOR(O)) + _ejs_throw_nativeerror_utf8(EJS_TYPE_ERROR, ".return called on non-generator"); + + EJSGenerator* gen = (EJSGenerator*)EJSVAL_TO_OBJECT(O); + ejsval arg = argc > 0 ? args[0] : _ejs_undefined; + + // not yet started, or already done: complete without running the body + if (!gen->started || gen->completed) { + gen->completed = EJS_TRUE; + return _ejs_create_iter_result(arg, _ejs_true); + } + + // suspended at a yield: resume with the return sentinel. finally + // blocks run; unless one of them yields or overrides the completion, + // the body's outer catch returns `arg` and the generator completes. + gen->returning = EJS_TRUE; + gen->yielded_value = _ejs_undefined; + gen->sent_value = arg; + _ejs_gc_remember(gen, gen->sent_value); + gen->caller_stack_top = (void*)&gen; // GC: the suspended segment starts here + swapcontext(&gen->caller_context, &gen->generator_context); + return _ejs_generator_resume_result(gen); } static EJS_NATIVE_FUNC(_ejs_Generator_prototype_next) { @@ -210,6 +329,12 @@ static EJS_NATIVE_FUNC(_ejs_Generator_prototype_next) { if (!EJSVAL_IS_GENERATOR(O)) _ejs_throw_nativeerror_utf8(EJS_TYPE_ERROR, ".next called on non-generator"); + EJSGenerator* gen = (EJSGenerator*)EJSVAL_TO_OBJECT(O); + // 25.3.3.3: a completed generator keeps answering { undefined, true } + // (resuming the dead context would be undefined behavior) + if (gen->completed) + return _ejs_create_iter_result(_ejs_undefined, _ejs_true); + return _ejs_generator_send(O, argc > 0 ? args[0] : _ejs_undefined); } @@ -221,7 +346,12 @@ ejsval _ejs_Iterator_prototype EJSVAL_ALIGNMENT; void _ejs_iterator_init_proto() { - _ejs_gc_add_root (&_ejs_Generator_prototype); + // used to (erroneously) root _ejs_Generator_prototype here, which + // _ejs_generator_init roots itself. nothing reachable references the + // iterator prototype until the other iterator protos are created, so + // without this root the first collection after this function freed it + // out from under everything that later used it as [[Prototype]]. + _ejs_gc_add_root (&_ejs_Iterator_prototype); _ejs_Iterator_prototype = _ejs_object_new(_ejs_Object_prototype, &_ejs_Object_specops); ejsval _iterator = _ejs_function_new_native (_ejs_null, _ejs_Symbol_iterator, _ejs_Iterator_prototype_iterator); @@ -237,6 +367,9 @@ _ejs_generator_init(ejsval global) _ejs_gc_add_root (&_ejs_Generator_prototype); _ejs_Generator_prototype = _ejs_object_new(_ejs_Iterator_prototype, &_ejs_Generator_specops); + _ejs_gc_add_root (&_ejs_generator_return_sentinel); + _ejs_generator_return_sentinel = _ejs_object_new(_ejs_null, &_ejs_Object_specops); + #define PROTO_METHOD(x) EJS_INSTALL_ATOM_FUNCTION_FLAGS (_ejs_Generator_prototype, x, _ejs_Generator_prototype_##x, EJS_PROP_NOT_ENUMERABLE | EJS_PROP_WRITABLE | EJS_PROP_CONFIGURABLE) PROTO_METHOD(next); @@ -252,49 +385,80 @@ _ejs_generator_specop_allocate() return (EJSObject*)_ejs_gc_new (EJSGenerator); } +// the live-generator registry — every generator's suspended +// stack must be conservatively scanned BEFORE a minor collection starts +// evacuating (see ejs-gc.c minor step 1) +EJSGenerator* _ejs_generator_registry; + static void _ejs_generator_specop_finalize (EJSObject* obj) { EJSGenerator* gen = (EJSGenerator*)obj; + if (gen->reg_next) gen->reg_next->reg_prev = gen->reg_prev; + if (gen->reg_prev) gen->reg_prev->reg_next = gen->reg_next; + if (_ejs_generator_registry == gen) _ejs_generator_registry = gen->reg_next; free (gen->stack); } -static void -_ejs_generator_specop_scan (EJSObject* obj, EJSValueFunc scan_func) +// the conservative half of the generator scan: both saved register +// files (the ucontexts) and the live suspended stack segment. Shared +// by the specop scan and the minor collection's pre-evacuation registry +// walk (conservative ranges must all be seen before any +// object moves). +void +_ejs_generator_scan_conservative (EJSGenerator* gen) { - EJSGenerator* gen = (EJSGenerator*)obj; - scan_func(gen->body); - scan_func(gen->yielded_value); - scan_func(gen->sent_value); - _ejs_gc_mark_conservative_range(&gen->generator_context, (char*)&gen->generator_context + sizeof(ucontext_t)); _ejs_gc_mark_conservative_range(&gen->caller_context, (char*)&gen->caller_context + sizeof(ucontext_t)); if (gen->stack) { - _ejs_gc_mark_conservative_range(gen->stack, + void* stack_end = gen->stack + gen->stack_size; + void* saved_sp = #if __APPLE__ #if TARGET_CPU_AMD64 - (void*)gen->generator_context.__mcontext_data.__ss.__rsp + (void*)gen->generator_context.__mcontext_data.__ss.__rsp #elif TARGET_CPU_X86 - (void*)gen->generator_context.__mcontext_data.__ss.__esp + (void*)gen->generator_context.__mcontext_data.__ss.__esp #elif TARGET_CPU_ARM - (void*)gen->generator_context.__mcontext_data.__ss.__sp -#elif TARGET_CPU_AARCH64 - (void*)gen->generator_context.__mcontext_data.__ss.__sp + (void*)gen->generator_context.__mcontext_data.__ss.__sp +#elif TARGET_CPU_ARM64 + (void*)gen->generator_context.__mcontext_data.__ss.__sp #else #error "unimplemented darwin cpu arch" #endif #elif linux #if TARGET_CPU_AMD64 - (void*)gen->generator_context.uc_mcontext.gregs[REG_RSP] + (void*)gen->generator_context.uc_mcontext.gregs[REG_RSP] +#elif TARGET_CPU_ARM64 + (void*)gen->generator_context.uc_mcontext.sp #else #error "unimplemented linux cpu arch" #endif #else #error "unimplemented platform" #endif - ); + ; + // The stack grows DOWN: the live suspended frames sit between the + // suspension SP and the stack's END. (This scan used to cover + // [stack, sp) — the dead region — and so missed every live frame; + // found the hard way.) An SP outside the range (never-started context, + // garbage) degrades to scanning the whole stack, which is merely + // conservative. + if (saved_sp < gen->stack || saved_sp > stack_end) + saved_sp = gen->stack; + _ejs_gc_mark_conservative_range(saved_sp, stack_end); } +} + +static void +_ejs_generator_specop_scan (EJSObject* obj, EJSValueFunc scan_func) +{ + EJSGenerator* gen = (EJSGenerator*)obj; + scan_func(&(gen->body)); + scan_func(&(gen->yielded_value)); + scan_func(&(gen->sent_value)); + + _ejs_generator_scan_conservative (gen); _ejs_Object_specops.Scan (obj, scan_func); } diff --git a/runtime/ejs-generator.h b/runtime/ejs-generator.h index a9d84a47..0b387595 100644 --- a/runtime/ejs-generator.h +++ b/runtime/ejs-generator.h @@ -13,7 +13,7 @@ EJS_BEGIN_DECLS #define EJSVAL_IS_GENERATOR(v) (EJSVAL_IS_OBJECT(v) && (EJSVAL_TO_OBJECT(v)->ops == &_ejs_Generator_specops)) -typedef struct { +typedef struct _EJSGenerator { /* object header */ EJSObject obj; @@ -27,12 +27,56 @@ typedef struct { // when true, we throw from the yield point. when false we simply return EJSBool throwing; + // when true, the resume is a .return(): the yield point throws the + // return sentinel (sent_value holds the return value) + EJSBool returning; + + // the body ran to completion (normally, or via the return sentinel); + // next/throw/return on a completed generator must not resume the + // dead context + EJSBool completed; + + // the body ended with an uncaught throw; yielded_value holds the + // exception, which the resume site rethrows on the CALLER's stack + // (unwinding it on the generator stack would walk off the + // makecontext frame) + EJSBool threw_out; + void* stack; + size_t stack_size; + + // all live generators sit on a registry so a minor + // collection can scan every suspended stack CONSERVATIVELY before + // any evacuation — a generator discovered mid-trace would pin its + // stack referents too late (they may already have moved) + struct _EJSGenerator* reg_next; + struct _EJSGenerator* reg_prev; + + // the caller-side stack position recorded just before each swap INTO + // this generator (the address of a local in the resuming frame). While + // the generator runs, its caller's frames live ABOVE this address (the + // stack grows down) — the GC scans [caller_stack_top, caller's stack + // end) to cover the suspended segment. + void* caller_stack_top; + + // each machine stack owns a disjoint gc-frame chain. + // The push hook parks the caller's chain head here and installs + // this generator's saved head (NULL on first entry); the pop hook + // does the reverse. While suspended, gc_frame_head is the walk + // root for this stack's precise frames; while running it is NULL + // (the live chain is _ejs_heap.gc_frame_head) and the caller's + // segment is reachable via caller_gc_frame_head. + void* gc_frame_head; + void* caller_gc_frame_head; ucontext_t generator_context; ucontext_t caller_context; } EJSGenerator; +extern ejsval _ejs_generator_return_sentinel; +ejsval _ejs_generator_is_return_sentinel (ejsval exc); +ejsval _ejs_generator_return_value (ejsval generator); + extern ejsval _ejs_IteratorWrapper_prototype; extern EJSSpecOps _ejs_IteratorWrapper_specops; @@ -56,6 +100,12 @@ extern void _ejs_iterator_init_proto (); extern void _ejs_gc_push_generator(EJSGenerator *gen); extern void _ejs_gc_pop_generator(); +/* the live-generator registry (ejs-generator.c) + the + conservative half of the generator scan, shared by the specop and the + minor collection's pre-evacuation pass */ +extern EJSGenerator* _ejs_generator_registry; +extern void _ejs_generator_scan_conservative(EJSGenerator* gen); + EJS_END_DECLS #endif diff --git a/runtime/ejs-init.c b/runtime/ejs-init.c index 8ab1a4b1..9c8ce501 100644 --- a/runtime/ejs-init.c +++ b/runtime/ejs-init.c @@ -44,6 +44,10 @@ #endif #include "ejs-proxy.h" #include "ejs-reflect.h" +#include "ejs-shapes.h" + +// lives in ejs-atoms-gen.c +extern void _ejs_init_static_strings(); const ejsval _ejs_undefined EJSVAL_ALIGNMENT = STATIC_BUILD_EJSVAL(EJSVAL_TAG_UNDEFINED, 0); ejsval _ejs_nan; @@ -57,8 +61,6 @@ const ejsval _ejs_one EJSVAL_ALIGNMENT = STATIC_BUILD_DOUBLE_EJSVAL(1); ejsval _ejs__ejs EJSVAL_ALIGNMENT; ejsval _ejs_global EJSVAL_ALIGNMENT; -/* useful strings literals */ -#include "ejs-atoms-gen.c" EJS_NATIVE_FUNC(_ejs_eval) { _ejs_throw_nativeerror_utf8 (EJS_ERROR, "EJS doesn't support eval()"); @@ -145,9 +147,193 @@ _ejs_init_classes() #endif } +// root every global ejsval the runtime stores builtins into. these +// statics live in the data segment, which the collector does not scan; +// relying on each *_init function to root (or connect to the object +// graph) whatever it creates proved fragile -- a missed root means the +// first collection frees an object that is still referenced (see +// _ejs_iterator_init_proto). registering a root for a still-zeroed +// ejsval is harmless. +static void +_ejs_root_builtin_globals(void) +{ + extern ejsval _ejs_Array; + extern ejsval _ejs_ArrayBuffer; + extern ejsval _ejs_ArrayIterator; + extern ejsval _ejs_Boolean; + extern ejsval _ejs_DataView; + extern ejsval _ejs_Date; + extern ejsval _ejs_Error; + extern ejsval _ejs_Error_prototype; + extern ejsval _ejs_EvalError; + extern ejsval _ejs_EvalError_prototype; + extern ejsval _ejs_Float32Array; + extern ejsval _ejs_Float32Array_prototype; + extern ejsval _ejs_Float64Array; + extern ejsval _ejs_Float64Array_prototype; + extern ejsval _ejs_Function; + extern ejsval _ejs_Int16Array; + extern ejsval _ejs_Int16Array_prototype; + extern ejsval _ejs_Int32Array; + extern ejsval _ejs_Int32Array_prototype; + extern ejsval _ejs_Int8Array; + extern ejsval _ejs_Int8Array_prototype; + extern ejsval _ejs_JSON; + extern ejsval _ejs_Map; + extern ejsval _ejs_MapIterator; + extern ejsval _ejs_Math; + extern ejsval _ejs_Number; + extern ejsval _ejs_Object; + extern ejsval _ejs_Process; + extern ejsval _ejs_Promise; + extern ejsval _ejs_Proxy; + extern ejsval _ejs_RangeError; + extern ejsval _ejs_RangeError_prototype; + extern ejsval _ejs_ReferenceError; + extern ejsval _ejs_ReferenceError_prototype; + extern ejsval _ejs_Reflect; + extern ejsval _ejs_RegExp; + extern ejsval _ejs_SetIterator; + extern ejsval _ejs_String; + extern ejsval _ejs_StringIterator; + extern ejsval _ejs_Symbol; + extern ejsval _ejs_Symbol_create; + extern ejsval _ejs_Symbol_hasInstance; + extern ejsval _ejs_Symbol_isConcatSpreadable; + extern ejsval _ejs_Symbol_iterator; + extern ejsval _ejs_Symbol_match; + extern ejsval _ejs_Symbol_replace; + extern ejsval _ejs_Symbol_search; + extern ejsval _ejs_Symbol_species; + extern ejsval _ejs_Symbol_split; + extern ejsval _ejs_Symbol_toPrimitive; + extern ejsval _ejs_Symbol_toStringTag; + extern ejsval _ejs_Symbol_unscopables; + extern ejsval _ejs_SyntaxError; + extern ejsval _ejs_SyntaxError_prototype; + extern ejsval _ejs_Timer; + extern ejsval _ejs_TypeError; + extern ejsval _ejs_TypeError_prototype; + extern ejsval _ejs_URIError; + extern ejsval _ejs_URIError_prototype; + extern ejsval _ejs_Uint16Array; + extern ejsval _ejs_Uint16Array_prototype; + extern ejsval _ejs_Uint32Array; + extern ejsval _ejs_Uint32Array_prototype; + extern ejsval _ejs_Uint8Array; + extern ejsval _ejs_Uint8Array_prototype; + extern ejsval _ejs_Uint8ClampedArray; + extern ejsval _ejs_Uint8ClampedArray_prototype; + extern ejsval _ejs_WeakMap; + extern ejsval _ejs_WeakSet; + extern ejsval _ejs__ejs; + extern ejsval _ejs_clearInterval; + extern ejsval _ejs_clearTimeout; + extern ejsval _ejs_console; + extern ejsval _ejs_decodeURI; + extern ejsval _ejs_decodeURIComponent; + extern ejsval _ejs_encodeURI; + extern ejsval _ejs_encodeURIComponent; + extern ejsval _ejs_isFinite; + extern ejsval _ejs_isNaN; + extern ejsval _ejs_parseFloat; + extern ejsval _ejs_parseInt; + extern ejsval _ejs_require; + extern ejsval _ejs_setInterval; + extern ejsval _ejs_setTimeout; + + _ejs_gc_add_root (&_ejs_Array); + _ejs_gc_add_root (&_ejs_ArrayBuffer); + _ejs_gc_add_root (&_ejs_ArrayIterator); + _ejs_gc_add_root (&_ejs_Boolean); + _ejs_gc_add_root (&_ejs_DataView); + _ejs_gc_add_root (&_ejs_Date); + _ejs_gc_add_root (&_ejs_Error); + _ejs_gc_add_root (&_ejs_Error_prototype); + _ejs_gc_add_root (&_ejs_EvalError); + _ejs_gc_add_root (&_ejs_EvalError_prototype); + _ejs_gc_add_root (&_ejs_Float32Array); + _ejs_gc_add_root (&_ejs_Float32Array_prototype); + _ejs_gc_add_root (&_ejs_Float64Array); + _ejs_gc_add_root (&_ejs_Float64Array_prototype); + _ejs_gc_add_root (&_ejs_Function); + _ejs_gc_add_root (&_ejs_Int16Array); + _ejs_gc_add_root (&_ejs_Int16Array_prototype); + _ejs_gc_add_root (&_ejs_Int32Array); + _ejs_gc_add_root (&_ejs_Int32Array_prototype); + _ejs_gc_add_root (&_ejs_Int8Array); + _ejs_gc_add_root (&_ejs_Int8Array_prototype); + _ejs_gc_add_root (&_ejs_JSON); + _ejs_gc_add_root (&_ejs_Map); + _ejs_gc_add_root (&_ejs_MapIterator); + _ejs_gc_add_root (&_ejs_Math); + _ejs_gc_add_root (&_ejs_Number); + _ejs_gc_add_root (&_ejs_Object); + _ejs_gc_add_root (&_ejs_Process); + _ejs_gc_add_root (&_ejs_Promise); + _ejs_gc_add_root (&_ejs_Proxy); + _ejs_gc_add_root (&_ejs_RangeError); + _ejs_gc_add_root (&_ejs_RangeError_prototype); + _ejs_gc_add_root (&_ejs_ReferenceError); + _ejs_gc_add_root (&_ejs_ReferenceError_prototype); + _ejs_gc_add_root (&_ejs_Reflect); + _ejs_gc_add_root (&_ejs_RegExp); + _ejs_gc_add_root (&_ejs_SetIterator); + _ejs_gc_add_root (&_ejs_String); + _ejs_gc_add_root (&_ejs_StringIterator); + _ejs_gc_add_root (&_ejs_Symbol); + _ejs_gc_add_root (&_ejs_Symbol_create); + _ejs_gc_add_root (&_ejs_Symbol_hasInstance); + _ejs_gc_add_root (&_ejs_Symbol_isConcatSpreadable); + _ejs_gc_add_root (&_ejs_Symbol_iterator); + _ejs_gc_add_root (&_ejs_Symbol_match); + _ejs_gc_add_root (&_ejs_Symbol_replace); + _ejs_gc_add_root (&_ejs_Symbol_search); + _ejs_gc_add_root (&_ejs_Symbol_species); + _ejs_gc_add_root (&_ejs_Symbol_split); + _ejs_gc_add_root (&_ejs_Symbol_toPrimitive); + _ejs_gc_add_root (&_ejs_Symbol_toStringTag); + _ejs_gc_add_root (&_ejs_Symbol_unscopables); + _ejs_gc_add_root (&_ejs_SyntaxError); + _ejs_gc_add_root (&_ejs_SyntaxError_prototype); + _ejs_gc_add_root (&_ejs_Timer); + _ejs_gc_add_root (&_ejs_TypeError); + _ejs_gc_add_root (&_ejs_TypeError_prototype); + _ejs_gc_add_root (&_ejs_URIError); + _ejs_gc_add_root (&_ejs_URIError_prototype); + _ejs_gc_add_root (&_ejs_Uint16Array); + _ejs_gc_add_root (&_ejs_Uint16Array_prototype); + _ejs_gc_add_root (&_ejs_Uint32Array); + _ejs_gc_add_root (&_ejs_Uint32Array_prototype); + _ejs_gc_add_root (&_ejs_Uint8Array); + _ejs_gc_add_root (&_ejs_Uint8Array_prototype); + _ejs_gc_add_root (&_ejs_Uint8ClampedArray); + _ejs_gc_add_root (&_ejs_Uint8ClampedArray_prototype); + _ejs_gc_add_root (&_ejs_WeakMap); + _ejs_gc_add_root (&_ejs_WeakSet); + _ejs_gc_add_root (&_ejs__ejs); + _ejs_gc_add_root (&_ejs_clearInterval); + _ejs_gc_add_root (&_ejs_clearTimeout); + _ejs_gc_add_root (&_ejs_console); + _ejs_gc_add_root (&_ejs_decodeURI); + _ejs_gc_add_root (&_ejs_decodeURIComponent); + _ejs_gc_add_root (&_ejs_encodeURI); + _ejs_gc_add_root (&_ejs_encodeURIComponent); + _ejs_gc_add_root (&_ejs_isFinite); + _ejs_gc_add_root (&_ejs_isNaN); + _ejs_gc_add_root (&_ejs_parseFloat); + _ejs_gc_add_root (&_ejs_parseInt); + _ejs_gc_add_root (&_ejs_require); + _ejs_gc_add_root (&_ejs_setInterval); + _ejs_gc_add_root (&_ejs_setTimeout); +} + void _ejs_init(int argc, char** argv) { + // shape tracking must be configured before the first object is created + _ejs_shapes_init(); + // process class inheritance _ejs_init_classes(); @@ -157,6 +343,8 @@ _ejs_init(int argc, char** argv) _ejs_gc_init(); _ejs_exception_init(); + _ejs_root_builtin_globals(); + // initialization or ECMA262 builtins _ejs_gc_add_root (&_ejs_global); _ejs_global = _ejs_object_new (_ejs_null, &_ejs_Object_specops); @@ -247,4 +435,12 @@ _ejs_init(int argc, char** argv) _ejs_gc_allocate_oom_exceptions(); EJS_INSTALL_ATOM_FUNCTION_FLAGS(_ejs__ejs, unhandledException, _ejs_unhandledException, 0); + + // builtin installs above (Object.prototype.__proto__ et al) predate + // user code and are audited against the virtualized-constructor + // contract (ejs-object.h): the only builtin accessor reachable from + // a fresh ordinary object's prototype chain is __proto__, a name the + // compiler's constructor fence never admits as a field. Everything + // after this point counts. + _ejs_accessor_epoch = 0; } diff --git a/runtime/ejs-invoke-closure-catch.ll b/runtime/ejs-invoke-closure-catch.ll index fe036cf6..42ddecaf 100644 --- a/runtime/ejs-invoke-closure-catch.ll +++ b/runtime/ejs-invoke-closure-catch.ll @@ -5,7 +5,7 @@ %EjsFuncType = type { } -define i32 @_ejs_invoke_closure_catch (%EjsValueType* nocapture %retval, %EjsValueType %closure, %EjsValueType* %_this, i32 %argc, %EjsValueType* nocapture readnone %args, %EjsValueType %newTarget) personality i8* bitcast (i32 (i32, i32, i64, i8*, i8*)* @__ejs_personality_v0 to i8*) { +define i32 @_ejs_invoke_closure_catch_inner (%EjsValueType* nocapture %retval, %EjsValueType %closure, %EjsValueType* %_this, i32 %argc, %EjsValueType* nocapture readnone %args, %EjsValueType %newTarget) personality i8* bitcast (i32 (i32, i32, i64, i8*, i8*)* @__ejs_personality_v0 to i8*) { entry: %rv_alloc = alloca i32 @@ -40,7 +40,7 @@ try_merge: ret i32 %rvload } -define i32 @_ejs_invoke_func_catch (%EjsValueType* nocapture %retval, i64 (i8*)* %func, i8* %data) personality i8* bitcast (i32 (i32, i32, i64, i8*, i8*)* @__ejs_personality_v0 to i8*) { +define i32 @_ejs_invoke_func_catch_inner (%EjsValueType* nocapture %retval, i64 (i8*)* %func, i8* %data) personality i8* bitcast (i32 (i32, i32, i64, i8*, i8*)* @__ejs_personality_v0 to i8*) { entry: %rv_alloc = alloca i32 diff --git a/runtime/ejs-json.c b/runtime/ejs-json.c index 00e2ed5f..ea4991ba 100644 --- a/runtime/ejs-json.c +++ b/runtime/ejs-json.c @@ -15,7 +15,7 @@ #include "ejs-string.h" #include "ejs-boolean.h" #include "ejs-symbol.h" -#include "../parson/parson.h" +#include "external-deps/parson/parson.h" ejsval _ejs_JSON EJSVAL_ALIGNMENT; diff --git a/runtime/ejs-map.c b/runtime/ejs-map.c index 1018db32..5d0fac2b 100644 --- a/runtime/ejs-map.c +++ b/runtime/ejs-map.c @@ -64,14 +64,25 @@ _ejs_map_delete (ejsval map, ejsval key) // our caller should have already validated and thrown appropriate TypeErrors EJS_ASSERT(EJSVAL_IS_MAP(map)); + EJSMap* _map = EJSVAL_TO_MAP(map); + // 4. Let entries be the List that is the value of M’s [[MapData]] internal slot. + EJSKeyValueEntry* entries = _map->head_insert; + // 5. Repeat for each Record {[[key]], [[value]]} p that is an element of entries, - // a. If p.[[key]] is not empty and SameValueZero(p.[[key]], key) is true, then - // i. Set p.[[key]] to empty. - // ii. Set p.[[value]] to empty. - // iii. Return true. - // 6. Return false. + for (EJSKeyValueEntry* p = entries; p; p = p->next_insert) { + // a. If p.[[key]] is not empty and SameValueZero(p.[[key]], key) is true, then + if (!EJSVAL_IS_NO_ITER_VALUE_MAGIC(p->key) && SameValueZero (p->key, key)) { + // i. Set p.[[key]] to empty. + p->key = MAGIC_TO_EJSVAL_IMPL(EJS_NO_ITER_VALUE); + // ii. Set p.[[value]] to empty. + p->value = MAGIC_TO_EJSVAL_IMPL(EJS_NO_ITER_VALUE); + // iii. Return true. + return _ejs_true; + } + } + // 6. Return false. return _ejs_false; } @@ -273,6 +284,7 @@ _ejs_map_set (ejsval map, ejsval key, ejsval value) if (!EJSVAL_IS_NO_ITER_VALUE_MAGIC(p->key) && SameValueZero (p->key, key)) { // i. Set p.[[value]] to value. p->value = value; + _ejs_gc_remember(_map, p->value); // ii. Return M. return map; } @@ -284,7 +296,9 @@ _ejs_map_set (ejsval map, ejsval key, ejsval value) // 7. Let p be the Record {[[key]]: key, [[value]]: value}. p = calloc (1, sizeof (EJSKeyValueEntry)); p->key = key; + _ejs_gc_remember(_map, p->key); p->value = value; + _ejs_gc_remember(_map, p->value); // 8. Append p as the last element of entries. if (!_map->head_insert) @@ -651,8 +665,8 @@ _ejs_map_specop_scan (EJSObject* obj, EJSValueFunc scan_func) EJSMap* map = (EJSMap*)obj; for (EJSKeyValueEntry *s = map->head_insert; s; s = s->next_insert) { - scan_func (s->key); - scan_func (s->value); + scan_func (&(s->key)); + scan_func (&(s->value)); } _ejs_Object_specops.Scan (obj, scan_func); @@ -682,7 +696,7 @@ static void _ejs_map_iterator_specop_scan (EJSObject* obj, EJSValueFunc scan_func) { EJSMapIterator* iter = (EJSMapIterator*)obj; - scan_func(iter->iterated); + scan_func(&(iter->iterated)); _ejs_Object_specops.Scan (obj, scan_func); } diff --git a/runtime/ejs-math.c b/runtime/ejs-math.c index cd9baa20..35e48b17 100644 --- a/runtime/ejs-math.c +++ b/runtime/ejs-math.c @@ -196,7 +196,16 @@ static EJS_NATIVE_FUNC(_ejs_Math_round) { if (isnan(x_)) return _ejs_nan; - return NUMBER_TO_EJSVAL(round (x_)); + // ES rounds ties toward +∞ (C round() ties away from zero). + // floor(x + 0.5) is exact on the remaining range: at |x| >= 2^52 + // there is no fractional part (and x + 0.5 could tie-to-even past + // an odd integer), and below 0.5 the addition can round up to 1.0 + // (x = 0.49999999999999994) — both screened off first. + if (fabs(x_) >= 4503599627370496.0 /* 2^52, also +-inf */) + return NUMBER_TO_EJSVAL(x_); + if (x_ >= -0.5 && x_ < 0.5) + return NUMBER_TO_EJSVAL(signbit(x_) ? -0.0 : 0.0); + return NUMBER_TO_EJSVAL(floor(x_ + 0.5)); } // ECMA262: 15.8.2.16 diff --git a/runtime/ejs-module.c b/runtime/ejs-module.c index 58149bca..822a5d5a 100644 --- a/runtime/ejs-module.c +++ b/runtime/ejs-module.c @@ -40,7 +40,7 @@ _ejs_module_specop_scan (EJSObject* obj, EJSValueFunc scan_func) EJSModule *module = (EJSModule*)obj; for (int i = 0; i < module->num_exports; i ++) - scan_func(module->exports[i]); + scan_func(&(module->exports[i])); _ejs_Object_specops.Scan (obj, scan_func); } diff --git a/runtime/ejs-object.c b/runtime/ejs-object.c index 467488d1..c693acdd 100644 --- a/runtime/ejs-object.c +++ b/runtime/ejs-object.c @@ -29,6 +29,8 @@ #include "ejs-symbol.h" #include "ejs-error.h" #include "ejs-xhr.h" +#include "ejs-shapes.h" +#include "ejs-closureenv.h" // ES6 7.3.1 // Get (O, P) @@ -420,7 +422,7 @@ _ejs_propertymap_foreach_value (EJSPropertyMap* map, EJSValueFunc foreach_func) { for (_EJSPropertyMapEntry *s = map->head_insert; s; s = s->next_insert) { if (_ejs_property_desc_has_value (s->desc)) - foreach_func(s->desc->value); + foreach_func(&(s->desc->value)); } } @@ -571,6 +573,305 @@ _ejs_propertymap_insert (EJSPropertyMap* map, ejsval name, EJSPropertyDesc* desc } } +// ------------------------------------------------------------------------ +// shaped-mode slot storage. Ordinary objects with a +// nonzero shape index keep their plain data property values in a +// closureenv slot array at shape-determined indices; the map only exists +// in dictionary mode. EJS_SHAPE_CAP is clamped to 256, so fixed +// 256-entry name buffers cover any shape. + +static EJSClosureEnv* +shaped_env (EJSObject* obj) +{ + return EJSVAL_TO_CLOSUREENV_IMPL(obj->slots); +} + +static ejsval* +shaped_slots (EJSObject* obj) +{ + return shaped_env(obj)->slots; +} + +// is the slot storage embedded in the object's own cell (single-cell +// born-with-shape allocation, gc-P5)? Pointer identity is the mode +// test — no header bit to keep coherent through evacuation's memcpy. +static EJSBool +shaped_slots_are_embedded (EJSObject* obj) +{ + return (char*)shaped_env(obj) == (char*)obj + sizeof(EJSObject); +} + +// retiring a slot-storage env: an OLD out-of-line env we are about to +// disconnect is old-gen garbage until the next full sweep, but the +// old-gen WALKERS — the minor's remset-overflow fallback and the +// EJS_GC_VERIFY/EJS_GC_PARANOID checkers — cannot tell garbage from +// live and will still visit its slots. Queue the retiree for one +// precise scan: the next minor rewrites its young refs (live right +// now, via the surviving copies) to their promoted addresses, after +// which the cell is inert until swept. (Found by the P6.3 stress +// lanes: the promoted env of a young rooted object, orphaned by +// capacity growth during _ejs_init, kept pre-promotion slot values +// that only ACCIDENTAL conservative pins of stale stack copies had +// been rescuing — the file split's codegen shift removed the luck.) +static void +shaped_retire_slots (EJSObject* obj) +{ + if (_ejs_heap.nursery_base == NULL) // nursery off: no remset + return; + if (EJSVAL_IS_NULL(obj->slots) || shaped_slots_are_embedded (obj)) + return; // embedded storage dies inside the object's own cell + EJSClosureEnv* env = shaped_env (obj); + if (_ejs_gc_is_young (env)) // young garbage is swept precisely + return; + if (*(GCObjectHeaderWord*)env & EJS_GC_HEADER_DIRTY) + return; // already queued + _ejs_gc_remember_slow (env); +} + +// grow slot storage to hold at least `needed` values. May allocate from +// the GC heap: obj->slots stays attached (and scanned) until the copy is +// done, so a collection triggered by the new array is safe. Growth is +// 4 -> 8 -> EJS_SHAPE_FIELD_CAP_MAX (=14): the final step lands exactly +// on the page allocator's largest (128-byte) cell so a slot array never +// reaches the LOS (whose linear lookup makes marking quadratic). +static void +shaped_ensure_capacity (EJSObject* obj, uint32_t needed) +{ + EJS_ASSERT(needed <= EJS_SHAPE_FIELD_CAP_MAX); + uint32_t cap = EJSVAL_IS_NULL(obj->slots) ? 0 : shaped_env(obj)->length; + if (needed <= cap) + return; + uint32_t newcap = cap ? cap * 2 : 4; + while (newcap < needed) + newcap *= 2; + if (newcap > EJS_SHAPE_FIELD_CAP_MAX) + newcap = EJS_SHAPE_FIELD_CAP_MAX; + ejsval newslots = _ejs_closureenv_new (newcap); + if (cap) { + memcpy (EJSVAL_TO_CLOSUREENV_IMPL(newslots)->slots, shaped_slots(obj), + cap * sizeof(ejsval)); + shaped_retire_slots (obj); + } + obj->slots = newslots; + _ejs_gc_remember(obj, newslots); +} + +// one-way migration to dictionary mode: materialize the map from the +// shape's fields + the slot array, then flip the header index. Nothing +// here allocates from the GC heap, so the union flip is atomic as far as +// the collector is concerned. +static void +_ejs_object_to_dictionary (EJSObject* obj, EJSShapeMigrateReason reason) +{ + uint32_t shape = EJS_OBJECT_SHAPE(obj); + EJS_ASSERT(shape != EJS_SHAPE_DICT); + + uint32_t nfields = _ejs_shape_field_count(shape); + ejsval names[256]; + EJS_ASSERT(nfields <= 256); + _ejs_shape_fields (shape, names); + + ejsval slotsval = obj->slots; + EJSPropertyMap* map = (EJSPropertyMap*)calloc (sizeof(EJSPropertyMap), 1); + _ejs_propertymap_init (map); + for (uint32_t i = 0; i < nfields; i ++) { + EJSPropertyDesc* desc = _ejs_propertydesc_new(); + _ejs_property_desc_set_value (desc, EJSVAL_TO_CLOSUREENV_IMPL(slotsval)->slots[i]); + _ejs_property_desc_set_writable (desc, EJS_TRUE); + _ejs_property_desc_set_enumerable (desc, EJS_TRUE); + _ejs_property_desc_set_configurable (desc, EJS_TRUE); + _ejs_propertymap_insert (map, names[i], desc); + } + // the union flip below disconnects the slot array — same + // retirement contract as shaped_ensure_capacity + shaped_retire_slots (obj); + obj->map = map; + _ejs_shape_object_migrate (obj, reason); +} + +// ------------------------------------------------------------------------ +// born-with-shape allocation. Compiled --types code +// batches an object literal's (or a fenced constructor prefix's) stores +// into one call carrying the field names (interned atoms) and values in +// source order. The TRUE shape is re-derived from the actual values via +// the transition memo (~one compare per field on the monomorphic path), +// so a wrong static repr claim can never mint a lying shape; whenever +// anything is off-script the call falls back to today's sequential +// generic sets, byte-for-byte. + +// would assigning any of names[0..nfields) run something other than a +// plain data-property creation on the receiver? Assignment is [[Set]]: +// a proto-chain accessor intercepts, and a non-writable proto data +// property silently swallows the write (sloppy mode) — both must take +// the sequential path. Shaped-mode protos hold only default writable +// data fields, so only dictionary-mode protos need their maps probed; +// any exotic proto bails conservatively. +static EJSBool +shaped_proto_intercepts (ejsval proto, uint32_t nfields, const ejsval* names) +{ + for (ejsval p = proto; EJSVAL_IS_OBJECT(p); p = EJSVAL_TO_OBJECT(p)->proto) { + EJSObject* po = EJSVAL_TO_OBJECT(p); + if (po->ops != &_ejs_Object_specops) + return EJS_TRUE; + if (EJS_OBJECT_SHAPE(po) != EJS_SHAPE_DICT) + continue; + for (uint32_t i = 0; i < nfields; i ++) { + EJSPropertyDesc* d = _ejs_propertymap_lookup (po->map, names[i]); + if (d && (IsAccessorDescriptor(d) || !_ejs_property_desc_is_writable(d))) + return EJS_TRUE; + } + } + return EJS_FALSE; +} + +// try to install names/values wholesale on an empty root-shaped ordinary +// object. EJS_FALSE (object untouched) means the caller must run the +// sequential generic path. +static EJSBool +try_fill_shaped (ejsval objval, uint32_t argc, const ejsval* names, ejsval* values) +{ + if (!_ejs_shapes_tracking || argc == 0 || argc > EJS_SHAPE_FIELD_CAP_MAX) + return EJS_FALSE; + EJSObject* obj = EJSVAL_TO_OBJECT(objval); + if (obj->ops != &_ejs_Object_specops) + return EJS_FALSE; + // only an empty, extensible, shaped object qualifies: anything else + // (dictionary mode, existing fields, freeze) owes the generic + // algorithm. The compiled fast arm is guarded on exactly this, but + // the check is one compare and makes the call safe under any caller. + if (EJS_OBJECT_SHAPE(obj) != EJS_SHAPE_ROOT || !EJS_OBJECT_IS_EXTENSIBLE(obj)) + return EJS_FALSE; + if (shaped_proto_intercepts (obj->proto, argc, names)) + return EJS_FALSE; + uint32_t shape = EJS_SHAPE_ROOT; + for (uint32_t i = 0; i < argc; i ++) { + EJSShapeMigrateReason reason; + shape = _ejs_shape_transition_add_fast (shape, names[i], values[i], &reason); + if (shape == EJS_SHAPE_DICT) + return EJS_FALSE; // index-looking key / cap / table full + } + shaped_ensure_capacity (obj, argc); + memcpy (shaped_slots(obj), values, argc * sizeof(ejsval)); + // the object is the barrier owner for shaped stores: its Scan walks + // the slot values directly (embedded storage has no cell of its own) + for (uint32_t _wb = 0; _wb < (uint32_t)argc; _wb++) + _ejs_gc_remember(obj, values[_wb]); + EJS_OBJECT_SET_SHAPE(obj, shape); + return EJS_TRUE; +} + +// single-cell born-with-shape allocation (gc-P5): object + embedded +// slot storage in one GC cell — obj header | ops | proto | slots ejsval +// pointing at obj+32 | embedded env header | slot values. The embedded +// region is a real EJSClosureEnv layout, so every slots consumer +// (shaped_slots, compiled slotRef addressing, the collector's range +// walks) is oblivious; embedded-ness is pointer identity. nfields > 0; +// no GC can run between the alloc and the last store below. +static ejsval +shaped_alloc_embedded (ejsval proto, uint32_t shape, uint32_t nfields, + const ejsval* values) +{ + size_t size = sizeof(EJSObject) + sizeof(EJSClosureEnv) + + (nfields - 1) * sizeof(ejsval); + EJSObject* obj = _ejs_gc_new_obj(EJSObject, size); + _ejs_init_object (obj, proto, &_ejs_Object_specops); + EJSClosureEnv* env = (EJSClosureEnv*)((char*)obj + sizeof(EJSObject)); + env->gc_header = EJS_SCAN_TYPE_CLOSUREENV; + env->length = nfields; + if (values) + memcpy (env->slots, values, nfields * sizeof(ejsval)); + else + for (uint32_t i = 0; i < nfields; i ++) + env->slots[i] = _ejs_undefined; + obj->slots = CLOSUREENV_TO_EJSVAL_IMPL(env); + EJS_OBJECT_SET_SHAPE(obj, shape); + return OBJECT_TO_EJSVAL(obj); +} + +// ordinary-construct support: allocate the ordinary `this` with +// embedded slot capacity for `hint` fields (0 = today's bare cell). +// The object is born empty and root-shaped either way; the hint only +// pre-sizes the storage so the constructor's fill stays in-cell. +ejsval +_ejs_object_new_with_slot_hint (ejsval proto, uint32_t hint) +{ + if (!_ejs_shapes_tracking || hint == 0 || hint > EJS_SHAPE_EMBED_FIELD_MAX) + return _ejs_object_new (proto, &_ejs_Object_specops); + return shaped_alloc_embedded (proto, EJS_SHAPE_ROOT, hint, NULL); +} + +// a statically-keyed object literal: allocate + install in one call +ejsval +_ejs_object_new_shaped (uint32_t argc, ejsval* names, ejsval* values) +{ + // derive the true shape from the actual values FIRST (pure — the + // transition memo makes it ~one compare per field), then birth + // object + storage as one cell. Anything off-script falls back to + // the two-cell fill / sequential path, byte-for-byte as before. + if (_ejs_shapes_tracking && argc > 0 && argc <= EJS_SHAPE_EMBED_FIELD_MAX + && !shaped_proto_intercepts (_ejs_Object_prototype, argc, names)) { + uint32_t shape = EJS_SHAPE_ROOT; + for (uint32_t i = 0; i < argc; i ++) { + EJSShapeMigrateReason reason; + shape = _ejs_shape_transition_add_fast (shape, names[i], values[i], &reason); + if (shape == EJS_SHAPE_DICT) + break; + } + if (shape != EJS_SHAPE_DICT) + return shaped_alloc_embedded (_ejs_Object_prototype, shape, argc, values); + } + ejsval obj = _ejs_object_create (_ejs_Object_prototype); + if (!try_fill_shaped (obj, argc, names, values)) { + for (uint32_t i = 0; i < argc; i ++) + _ejs_object_setprop (obj, names[i], values[i]); + } + return obj; +} + +// a fenced constructor's straight-line store prefix, batched onto the +// construct-allocated `this` (whose proto is F.prototype) +ejsval +_ejs_object_fill_shaped (ejsval objval, uint32_t argc, ejsval* names, ejsval* values) +{ + if (!EJSVAL_IS_OBJECT(objval) || !try_fill_shaped (objval, argc, names, values)) { + for (uint32_t i = 0; i < argc; i ++) + _ejs_object_setprop (objval, names[i], values[i]); + } + return objval; +} + +// shaped GetOwnProperty synthesizes the default data descriptor for a +// slot into a static ring. Entries are transient — valid until +// SYNTH_DESC_RING subsequent shaped GetOwnProperty hits — which the +// spec-algorithm callers respect (none holds a descriptor across more +// than a couple of lookups). Every path that would MUTATE a property +// through its descriptor migrates the object to dictionary mode first, +// so writes through synthesized descriptors cannot happen. The ring's +// ejsvals are registered as gc roots: callers may hold a descriptor +// across an allocating call. +#define SYNTH_DESC_RING 32 +static EJSPropertyDesc synth_descs[SYNTH_DESC_RING]; +static int synth_desc_next = -1; + +static EJSPropertyDesc* +shaped_synthesize_desc (ejsval value) +{ + if (synth_desc_next < 0) { + for (int i = 0; i < SYNTH_DESC_RING; i ++) { + synth_descs[i].value = _ejs_undefined; + synth_descs[i].setter = _ejs_undefined; + _ejs_gc_add_root (&synth_descs[i].value); + _ejs_gc_add_root (&synth_descs[i].setter); + } + synth_desc_next = 0; + } + EJSPropertyDesc* desc = &synth_descs[synth_desc_next]; + synth_desc_next = (synth_desc_next + 1) % SYNTH_DESC_RING; + desc->flags = EJS_PROP_FLAGS_VALUE_SET | EJS_PROP_WRITABLE | EJS_PROP_ENUMERABLE | EJS_PROP_CONFIGURABLE; + desc->value = value; + return desc; +} + /* property iterators */ struct _EJSPropertyIterator { EJSObject obj; @@ -600,10 +901,10 @@ _ejs_property_iterator_specop_scan (EJSObject* obj, EJSValueFunc scan_func) { EJSPropertyIterator *iter = (EJSPropertyIterator*)obj; - scan_func (iter->forObj); + scan_func (&(iter->forObj)); for (int i = 0; i < iter->num; i ++) { - scan_func (iter->keys[i]); + scan_func (&(iter->keys[i])); } } @@ -645,6 +946,26 @@ collect_keys (ejsval objval, int *num, int *alloc, ejsval **keys) EJSObject *obj = EJSVAL_TO_OBJECT(objval); EJS_ASSERT(obj); + // shaped mode: shaped objects enumerate the shape chain (all fields + // are enumerable by construction; the chain is insertion order) + uint32_t shape = EJS_OBJECT_SHAPE(obj); + if (shape != EJS_SHAPE_DICT) { + uint32_t nfields = _ejs_shape_field_count(shape); + ejsval names[256]; + _ejs_shape_fields (shape, names); + for (uint32_t i = 0; i < nfields; i ++) { + if (!name_in_keys (names[i], *keys, *num)) { + if (*num == *alloc-1) { + (*alloc) += 10; + *keys = (ejsval*)realloc (*keys, (*alloc) * sizeof(ejsval)); + } + (*keys)[(*num)++] = names[i]; + } + } + collect_keys (obj->proto, num, alloc, keys); + return; + } + for (_EJSPropertyMapEntry *s = obj->map->head_insert; s; s = s->next_insert) { if (_ejs_property_desc_is_enumerable (s->desc) && !name_in_keys (s->name, *keys, *num)) { if (*num == *alloc-1) { @@ -754,9 +1075,18 @@ _ejs_init_object (EJSObject* obj, ejsval proto, EJSSpecOps *ops) { obj->proto = proto; obj->ops = ops ? ops : &_ejs_Object_specops; - obj->map = calloc (sizeof(EJSPropertyMap), 1); - _ejs_propertymap_init (obj->map); - //printf ("obj->map = %p\n", obj->map); + // shaped mode: ordinary objects are born with the root shape and + // lazily-allocated slot storage — no map calloc on this path; + // everything else (and every object under EJS_SHAPES=off) is + // dictionary-mode from birth + if (obj->ops == &_ejs_Object_specops && _ejs_shapes_tracking) { + obj->slots = _ejs_null; + _ejs_shape_object_born (obj); + } + else { + obj->map = calloc (sizeof(EJSPropertyMap), 1); + _ejs_propertymap_init (obj->map); + } EJS_OBJECT_SET_EXTENSIBLE(obj); #if notyet ((GCObjectPtr)obj)->gc_data = 0x01; // HAS_FINALIZE @@ -890,6 +1220,18 @@ _ejs_object_define_accessor_property (ejsval obj, ejsval key, ejsval get, ejsval return OP(_obj,DefineOwnProperty)(obj, key, &desc, EJS_FALSE); } +// like _ejs_object_define_accessor_property, but the caller's flags say +// which of get/set are present — a partial descriptor merges into an +// existing accessor property (`{ get [k]() {}, set [k](v) {} }` defines +// the getter and setter in two separate evaluations) +EJSBool +_ejs_object_define_accessor_property_desc (ejsval obj, ejsval key, ejsval get, ejsval set, uint32_t flags) +{ + EJSObject *_obj = EJSVAL_TO_OBJECT(obj); + EJSPropertyDesc desc = { .getter = get, .setter = set, .flags = flags }; + return OP(_obj,DefineOwnProperty)(obj, key, &desc, EJS_FALSE); +} + ejsval _ejs_object_setprop_utf8 (ejsval val, const char *key, ejsval value) @@ -955,6 +1297,10 @@ ejsval _ejs_Object EJSVAL_ALIGNMENT; ejsval _ejs_Object__proto__ EJSVAL_ALIGNMENT; ejsval _ejs_Object_prototype EJSVAL_ALIGNMENT; +// starts nonzero so a check that somehow runs before init completes +// fails closed; _ejs_init zeroes it once the builtins are in place +uint64_t _ejs_accessor_epoch = 1; + // ES2015, June 2015 // 19.1.1.1 Object ( [ value ] ) static EJS_NATIVE_FUNC(_ejs_Object_impl) { @@ -1036,6 +1382,16 @@ _ejs_object_set_prototype_of (ejsval obj, ejsval proto) return _ejs_Object_setPrototypeOf(_ejs_undefined, &undef_this, 2, args, _ejs_undefined); } +// `__proto__: value` in an object literal: set the prototype when value +// is an object or null, silently ignore anything else +// (PropertyDefinitionEvaluation / B.3.1) +ejsval +_ejs_object_literal_set_proto (ejsval obj, ejsval proto) +{ + if (!EJSVAL_IS_OBJECT(proto) && !EJSVAL_IS_NULL(proto)) return obj; + return _ejs_object_set_prototype_of (obj, proto); +} + // ECMA262: 19.1.2.6 Object.getOwnPropertyDescriptor ( O, P ) static EJS_NATIVE_FUNC(_ejs_Object_getOwnPropertyDescriptor) { ejsval O = _ejs_undefined; @@ -1060,34 +1416,60 @@ static EJS_NATIVE_FUNC(_ejs_Object_getOwnPropertyDescriptor) { return FromPropertyDescriptor(desc); } -// ECMA262: 19.1.2.7 Object.getOwnPropertyNames ( O ) +// ECMA262: 19.1.2.7 Object.getOwnPropertyNames ( O ) static EJS_NATIVE_FUNC(_ejs_Object_getOwnPropertyNames) { ejsval O = _ejs_undefined; if (argc > 0) O = args[0]; - /* 1. If Type(O) is not Object throw a TypeError exception. */ - if (!EJSVAL_IS_OBJECT(O)) { - _ejs_log ("throw TypeError, _this isn't an Object\n"); - EJS_NOT_IMPLEMENTED(); - } - EJSObject* O_ = EJSVAL_TO_OBJECT(O); + /* 1. Let obj be ToObject(O) (ES6: primitives coerce; null and + undefined throw). */ + ejsval obj = ToObject(O); + EJSObject* O_ = EJSVAL_TO_OBJECT(obj); /* 2. Let array be the result of creating a new object as if by the expression new Array () where Array is the standard built-in constructor with that name. */ ejsval arr = _ejs_array_new(0, EJS_FALSE); /* 3. Let n be 0. */ - /* 4. For each named own property P of O */ - for (_EJSPropertyMapEntry* s = O_->map->head_insert; s; s = s->next_insert) { - if (!_ejs_property_desc_is_enumerable(s->desc)) - continue; + // integer indices come first (OrdinaryOwnPropertyKeys order). + // Arrays and String objects keep their elements outside the + // property map, and both expose a virtual `length`. + if (EJSVAL_IS_ARRAY(obj)) { + _ejs_array_push_own_index_names(obj, arr); + ejsval length_name = _ejs_atom_length; + _ejs_array_push_dense(arr, 1, &length_name); + } + else if (EJSVAL_IS_STRING_OBJECT(obj)) { + ejsval prim = ((EJSString*)O_)->primStr; + for (int64_t i = 0; i < EJSVAL_TO_STRLEN(prim); i ++) { + ejsval idx_name = ToString(NUMBER_TO_EJSVAL(i)); + _ejs_array_push_dense(arr, 1, &idx_name); + } + ejsval length_name = _ejs_atom_length; + _ejs_array_push_dense(arr, 1, &length_name); + } + + // shaped mode: shaped objects report their (all-enumerable, + // string-keyed) shape fields in insertion order + uint32_t O_shape = EJS_OBJECT_SHAPE(O_); + if (O_shape != EJS_SHAPE_DICT) { + uint32_t nfields = _ejs_shape_field_count(O_shape); + ejsval names[256]; + _ejs_shape_fields (O_shape, names); + for (uint32_t i = 0; i < nfields; i ++) + _ejs_array_push_dense(arr, 1, &names[i]); + return arr; + } + /* 4. For each named own property P of O (enumerable or not — + only Object.keys/enumeration filter on the enumerable bit) */ + for (_EJSPropertyMapEntry* s = O_->map->head_insert; s; s = s->next_insert) { /* a. Let name be the String value that is the name of P. */ ejsval name = s->name; if (!EJSVAL_IS_SYMBOL(name)) { /* b. Call the [[DefineOwnProperty]] internal method of array with arguments ToString(n), the - PropertyDescriptor {[[Value]]: name, [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: + PropertyDescriptor {[[Value]]: name, [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: true}, and false. */ _ejs_array_push_dense(arr, 1, &name); } @@ -1112,12 +1494,16 @@ static EJS_NATIVE_FUNC(_ejs_Object_getOwnPropertySymbols) { } EJSObject* O_ = EJSVAL_TO_OBJECT(O); - /* 2. Let array be the result of creating a new object as if by the expression new Array () where Array is the + /* 2. Let array be the result of creating a new object as if by the expression new Array () where Array is the standard built-in constructor with that name. */ ejsval arr = _ejs_array_new(0, EJS_FALSE); /* 3. Let n be 0. */ + // shaped mode: shaped objects never carry symbol-keyed properties + if (EJS_OBJECT_SHAPE(O_) != EJS_SHAPE_DICT) + return arr; + /* 4. For each named own property P of O */ for (_EJSPropertyMapEntry* s = O_->map->head_insert; s; s = s->next_insert) { if (!_ejs_property_desc_is_enumerable(s->desc)) @@ -1150,8 +1536,6 @@ static EJS_NATIVE_FUNC(_ejs_Object_assign) { // 2. ReturnIfAbrupt(to). ejsval to = ToObject(target); - EJSObject* to_ = EJSVAL_TO_OBJECT(to); - // 3. If fewer than two arguments were passed,then return to. if (argc < 2) return to; @@ -1181,10 +1565,24 @@ static EJS_NATIVE_FUNC(_ejs_Object_assign) { // i. Let gotAllNames be false. //EJSBool gotAllNames = EJS_FALSE; XXX this is unused - // j. Let pendingException be undefined. + // j. Let pendingException be undefined. ejsval pendingException = _ejs_undefined; - // k. Repeat while nextIndex < len, + // shaped mode: a shaped source enumerates its shape fields (all + // enumerable plain data properties, in insertion order) + uint32_t from_shape = EJS_OBJECT_SHAPE(from_); + if (from_shape != EJS_SHAPE_DICT) { + uint32_t nfields = _ejs_shape_field_count(from_shape); + ejsval names[256]; + _ejs_shape_fields (from_shape, names); + for (uint32_t i = 0; i < nfields; i ++) { + ejsval propValue = OP(from_,Get)(from, names[i], from); + Put(to, names[i], propValue, EJS_TRUE); + } + continue; + } + + // k. Repeat while nextIndex < len, for (_EJSPropertyMapEntry* s = from_->map->head_insert; s; s = s->next_insert) { // i. Let nextKey be Get(keysArray, ToString(nextIndex)). // ii. ReturnIfAbrupt(nextKey). @@ -1240,6 +1638,7 @@ static EJS_NATIVE_FUNC(_ejs_Object_create) { /* 3. Set the [[Prototype]] internal property of obj to O. */ EJSVAL_TO_OBJECT(obj)->proto = O; + _ejs_gc_remember(EJSVAL_TO_OBJECT(obj), O); /* 4. If the argument Properties is present and not undefined, add own properties to obj as if by calling the */ /* standard built-in function Object.defineProperties with arguments obj and Properties. */ @@ -1280,7 +1679,6 @@ static EJS_NATIVE_FUNC(_ejs_Object_defineProperty) { free (utf8_name); _ejs_throw_nativeerror_utf8 (EJS_TYPE_ERROR, msg); } - EJSObject *obj = EJSVAL_TO_OBJECT(O); // 2. Let key be ToPropertyKey(P). // 3. ReturnIfAbrupt(key). @@ -1331,21 +1729,35 @@ static EJS_NATIVE_FUNC(_ejs_Object_defineProperties) { /* 3. Let names be an internal list containing the names of each enumerable own property of props. */ int names_len = 0; - for (_EJSPropertyMapEntry *s = props_obj->map->head_insert; s; s = s->next_insert) { - if (_ejs_property_desc_is_enumerable (s->desc)) - names_len ++; + ejsval* names; + // shaped mode: a shaped props object enumerates its shape fields + uint32_t props_shape = EJS_OBJECT_SHAPE(props_obj); + if (props_shape != EJS_SHAPE_DICT) { + names_len = (int)_ejs_shape_field_count(props_shape); + if (names_len == 0) { + /* no enumerable properties, bail early */ + return O; + } + names = malloc(names_len * sizeof(ejsval)); + _ejs_shape_fields (props_shape, names); } + else { + for (_EJSPropertyMapEntry *s = props_obj->map->head_insert; s; s = s->next_insert) { + if (_ejs_property_desc_is_enumerable (s->desc)) + names_len ++; + } - if (names_len == 0) { - /* no enumerable properties, bail early */ - return O; - } + if (names_len == 0) { + /* no enumerable properties, bail early */ + return O; + } - ejsval* names = malloc(names_len * sizeof(ejsval)); - int n = 0; - for (_EJSPropertyMapEntry *s = props_obj->map->head_insert; s; s = s->next_insert) { - if (_ejs_property_desc_is_enumerable(s->desc)) - names[n++] = s->name; + names = malloc(names_len * sizeof(ejsval)); + int n = 0; + for (_EJSPropertyMapEntry *s = props_obj->map->head_insert; s; s = s->next_insert) { + if (_ejs_property_desc_is_enumerable(s->desc)) + names[n++] = s->name; + } } /* 4. Let descriptors be an empty internal List. */ @@ -2009,7 +2421,12 @@ _ejs_object_specop_set_prototype_of (ejsval O, ejsval V) // 9. Set the value of the [[Prototype]] internal slot of O to V. + // A prototype swap can introduce intercepting properties (or an + // exotic object) into some fresh object's [[Set]] path — retire the + // virtualized-constructor fast path (ejs-object.h). + _ejs_accessor_epoch++; O_->proto = V; + _ejs_gc_remember(O_, V); // 10. Return true. return EJS_TRUE; @@ -2025,9 +2442,23 @@ _ejs_object_specop_get (ejsval O, ejsval P, ejsval Receiver) if (EJSVAL_IS_STRING(pname) && !ucs2_strcmp(_ejs_ucs2___proto__, EJSVAL_TO_FLAT_STRING(pname))) return OP(EJSVAL_TO_OBJECT(O),GetPrototypeOf) (O); - // 2. Let desc be the result of calling the [[GetOwnProperty]] internal method of O with argument P. - // 3. ReturnIfAbrupt(desc). - EJSPropertyDesc* desc = OP(EJSVAL_TO_OBJECT(O),GetOwnProperty) (O, P, NULL); + // 2. Let desc be the result of calling the [[GetOwnProperty]] internal method of O with argument P. + // 3. ReturnIfAbrupt(desc). + EJSPropertyDesc* desc; + EJSObject* O_ = EJSVAL_TO_OBJECT(O); + uint32_t O_shape = EJS_OBJECT_SHAPE(O_); + if (O_shape != EJS_SHAPE_DICT) { + // shaped-mode fast path: a hit is a fixed-index slot load; a + // miss (including symbol keys, which shaped objects never + // carry) falls to the proto walk below + uint32_t slot; + if (EJSVAL_IS_STRING(pname) && _ejs_shape_lookup (O_shape, pname, &slot)) + return shaped_slots(O_)[slot]; + desc = NULL; + } + else { + desc = OP(O_,GetOwnProperty) (O, P, NULL); + } // 4. If desc is undefined, then if (desc == NULL) { @@ -2071,6 +2502,18 @@ _ejs_object_specop_get_own_property (ejsval obj, ejsval propertyName, ejsval* ex ejsval property_str = ToPropertyKey(propertyName); EJSObject* obj_ = EJSVAL_TO_OBJECT(obj); + // shaped mode: shaped objects synthesize the default data + // descriptor from the slot (their fields are always plain + // writable/enumerable/configurable string-keyed data properties) + uint32_t shape = EJS_OBJECT_SHAPE(obj_); + if (shape != EJS_SHAPE_DICT) { + uint32_t slot; + if (EJSVAL_IS_STRING(property_str) && + _ejs_shape_lookup (shape, property_str, &slot)) + return shaped_synthesize_desc (shaped_slots(obj_)[slot]); + return NULL; + } + return _ejs_propertymap_lookup (obj_->map, property_str); } @@ -2080,10 +2523,34 @@ _ejs_object_specop_set (ejsval O, ejsval P, ejsval V, ejsval Receiver) { EJSPropertyDesc undefined_desc = { .value = _ejs_undefined, .flags = EJS_PROP_FLAGS_VALUE_SET | EJS_PROP_WRITABLE | EJS_PROP_ENUMERABLE | EJS_PROP_CONFIGURABLE }; - // 1. Assert: IsPropertyKey(P) is true. + // 1. Assert: IsPropertyKey(P) is true. P = ToPropertyKey(P); // XXX this shouldn't be necessary, but ejs passes numbers here - - // 2. Let ownDesc be the result of calling the [[GetOwnProperty]] internal method of O with argument P. + + // shaped-mode fast path: a store to an existing shaped field on the + // receiver itself is a repr check + slot store (shaped fields are + // always plain writable data properties). Absent fields take the + // generic path below — its proto walk and CreateDataProperty + // ending land back in the shaped DefineOwnProperty. + if (EJSVAL_EQ(O, Receiver)) { + EJSObject* O_ = EJSVAL_TO_OBJECT(O); + uint32_t O_shape = EJS_OBJECT_SHAPE(O_); + uint32_t slot; + if (O_shape != EJS_SHAPE_DICT && EJSVAL_IS_STRING(P) && + _ejs_shape_lookup (O_shape, P, &slot)) { + uint32_t next_shape = _ejs_shape_transition_set (O_shape, slot, V); + if (next_shape != EJS_SHAPE_DICT) { + EJS_OBJECT_SET_SHAPE(O_, next_shape); + shaped_slots(O_)[slot] = V; + _ejs_gc_remember(O_, V); + return EJS_TRUE; + } + // shape-table overflow: drop to dictionary mode and let the + // generic path store through the map + _ejs_object_to_dictionary (O_, EJS_SHAPE_MIGRATE_TABLE_FULL); + } + } + + // 2. Let ownDesc be the result of calling the [[GetOwnProperty]] internal method of O with argument P. // 3. ReturnIfAbrupt(ownDesc). EJSPropertyDesc* ownDesc = OP(EJSVAL_TO_OBJECT(O),GetOwnProperty)(O, P, NULL); @@ -2188,6 +2655,9 @@ _ejs_object_specop_delete (ejsval O, ejsval P, EJSBool Throw) /* 3. If desc.[[Configurable]] is true, then */ if (_ejs_property_desc_is_configurable(desc)) { /* a. Remove the own property with name P from O. */ + // shaped mode: deletes are a dictionary-mode affair + if (EJS_OBJECT_SHAPE(obj) != EJS_SHAPE_DICT) + _ejs_object_to_dictionary (obj, EJS_SHAPE_MIGRATE_DELETE); _ejs_propertymap_remove (obj->map, P); /* b. Return true. */ return EJS_TRUE; @@ -2212,6 +2682,97 @@ _ejs_object_specop_define_own_property (ejsval O, ejsval P, EJSPropertyDesc* Des EJS_MACRO_END EJSObject* obj = EJSVAL_TO_OBJECT(O); + + // the object-remembering barrier contract: every storage path below — + // shaped slot, map insert, in-place descriptor update — installs + // these values somewhere in obj's owned storage. Marking up front + // is at worst conservative (a rejected define dirties one object + // for one cycle). + _ejs_gc_remember(obj, P); + if (_ejs_property_desc_has_value(Desc)) _ejs_gc_remember(obj, Desc->value); + if (_ejs_property_desc_has_getter(Desc)) _ejs_gc_remember(obj, Desc->getter); + if (_ejs_property_desc_has_setter(Desc)) _ejs_gc_remember(obj, Desc->setter); + + // a descriptor that could intercept a later [[Set]] through the + // prototype chain — an accessor, or a non-writable data property — + // retires the virtualized-constructor fast path (ejs-object.h). + // Only ORDINARY receivers count: a virtualized instance's chain is + // ctor.prototype -> Object.prototype, both ordinary, and any other + // object can only join such a chain through a [[SetPrototypeOf]] + // (which bumps unconditionally) or a ctor.prototype swap (which the + // compiler declines statically). Without this screen the fast path + // would die at startup: every closure's non-writable name/length + // and every module's export accessors land here. Bumping on a + // define that ends up rejected is merely conservative. + if (obj->ops == &_ejs_Object_specops && + (_ejs_property_desc_has_getter(Desc) || _ejs_property_desc_has_setter(Desc) || + (_ejs_property_desc_has_writable(Desc) && !_ejs_property_desc_is_writable(Desc)))) + _ejs_accessor_epoch++; + + // shaped mode: route shaped objects up front. Plain default- + // attribute data properties live in slot storage; anything the + // shaped world can't express migrates to dictionary mode and falls + // into the generic algorithm below. (Absent property on a + // non-extensible object also falls through: the generic step 3 + // rejects without touching storage.) + uint32_t obj_shape = EJS_OBJECT_SHAPE(obj); + if (obj_shape != EJS_SHAPE_DICT) { + if (_ejs_property_desc_has_getter(Desc) || _ejs_property_desc_has_setter(Desc)) + _ejs_object_to_dictionary (obj, EJS_SHAPE_MIGRATE_ACCESSOR); + else if (!EJSVAL_IS_STRING(P)) + _ejs_object_to_dictionary (obj, EJS_SHAPE_MIGRATE_SYMBOL_KEY); + else { + uint32_t slot; + if (_ejs_shape_lookup (obj_shape, P, &slot)) { + // existing field: attribute-lowering migrates; a value + // update is a repr check + slot store (attributes are + // all true already, so re-asserting them is a no-op) + if ((_ejs_property_desc_has_writable(Desc) && !_ejs_property_desc_is_writable(Desc)) || + (_ejs_property_desc_has_enumerable(Desc) && !_ejs_property_desc_is_enumerable(Desc)) || + (_ejs_property_desc_has_configurable(Desc) && !_ejs_property_desc_is_configurable(Desc))) + _ejs_object_to_dictionary (obj, EJS_SHAPE_MIGRATE_ATTRS); + else if (_ejs_property_desc_has_value(Desc)) { + ejsval value = _ejs_property_desc_get_value(Desc); + uint32_t next_shape = _ejs_shape_transition_set (obj_shape, slot, value); + if (next_shape == EJS_SHAPE_DICT) + _ejs_object_to_dictionary (obj, EJS_SHAPE_MIGRATE_TABLE_FULL); + else { + EJS_OBJECT_SET_SHAPE(obj, next_shape); + shaped_slots(obj)[slot] = value; + _ejs_gc_remember(obj, value); + return EJS_TRUE; + } + } + else + return EJS_TRUE; + } + else if (EJS_OBJECT_IS_EXTENSIBLE(obj)) { + // absent field: only a creation with all-default + // attributes stays shaped (absent attribute fields + // default to false per the spec's step 4a) + if (!_ejs_property_desc_is_writable(Desc) || + !_ejs_property_desc_is_enumerable(Desc) || + !_ejs_property_desc_is_configurable(Desc)) + _ejs_object_to_dictionary (obj, EJS_SHAPE_MIGRATE_ATTRS); + else { + ejsval value = _ejs_property_desc_get_value(Desc); + EJSShapeMigrateReason reason; + uint32_t next_shape = _ejs_shape_transition_add_fast (obj_shape, P, value, &reason); + if (next_shape == EJS_SHAPE_DICT) + _ejs_object_to_dictionary (obj, reason); + else { + uint32_t nfields = _ejs_shape_field_count(next_shape); + shaped_ensure_capacity (obj, nfields); + EJS_OBJECT_SET_SHAPE(obj, next_shape); + shaped_slots(obj)[nfields - 1] = value; + _ejs_gc_remember(obj, value); + return EJS_TRUE; + } + } + } + } + } + /* 1. Let current be the result of calling the [[GetOwnProperty]] internal method of O with property name P. */ EJSPropertyDesc* current = OP(obj, GetOwnProperty)(O, P, NULL); @@ -2383,35 +2944,71 @@ _ejs_object_specop_allocate () return _ejs_gc_new(EJSObject); } -void +void _ejs_object_specop_finalize(EJSObject* obj) { - //printf ("_ejs_propertymap_free(obj->map = %p)\n", obj->map); - _ejs_propertymap_free (obj->map); + _ejs_shape_object_died (obj); + // shaped mode: shaped objects have no map; their slot array is GC + // memory and needs no finalization + if (EJS_OBJECT_SHAPE(obj) == EJS_SHAPE_DICT && obj->map) + _ejs_propertymap_free (obj->map); obj->map = NULL; } +// walk the entries directly so every scanned slot is the +// REAL storage location (the old foreach_property shim passed the name +// by value — a moved name's rewrite would have landed in a local copy). +// Property names are content-hashed, so a moving name never invalidates +// the buckets; descs are malloc'd and stay put. static void -scan_property (ejsval name, EJSPropertyDesc *desc, EJSValueFunc scan_func) +scan_property_entries (EJSPropertyMap* map, EJSValueFunc scan_func) { - scan_func (name); + for (_EJSPropertyMapEntry *s = map->head_insert; s; s = s->next_insert) { + scan_func (&s->name); - if (_ejs_property_desc_has_value (desc)) { - scan_func (desc->value); - } - if (_ejs_property_desc_has_getter (desc)) { - scan_func (desc->getter); - } - if (_ejs_property_desc_has_setter (desc)) { - scan_func (desc->setter); + if (_ejs_property_desc_has_value (s->desc)) { + scan_func (&s->desc->value); + } + if (_ejs_property_desc_has_getter (s->desc)) { + scan_func (&s->desc->getter); + } + if (_ejs_property_desc_has_setter (s->desc)) { + scan_func (&s->desc->setter); + } } } static void _ejs_object_specop_scan (EJSObject* obj, EJSValueFunc scan_func) { - _ejs_propertymap_foreach_property (obj->map, (EJSPropertyDescFunc)scan_property, scan_func); - scan_func (obj->proto); + // shaped mode: walk the slot VALUES directly — the object is the + // barrier owner for shaped stores, so a dirty-object rescan must + // see them, and embedded storage has no cell of its own. Field + // names are rooted by the global shape table. + uint32_t obj_shape = EJS_OBJECT_SHAPE(obj); + if (obj_shape != EJS_SHAPE_DICT) { + if (!EJSVAL_IS_NULL(obj->slots)) { + EJSClosureEnv* env = shaped_env(obj); + // the shape's trace bitmap (gc-P5): f64-repr slots hold raw + // doubles — never references — so the walk skips them. + // Slots past field_count (hint slack) are undefined, whose + // mask bits are 0, so they scan as the no-ops they are. + uint32_t f64_mask = _ejs_shape_get(obj_shape)->f64_mask; + for (uint32_t i = 0; i < env->length; i ++) + if (!(f64_mask & (1u << i))) + scan_func (&env->slots[i]); + // out-of-line storage is a real cell: scan the edge so the + // env itself stays alive and the reference moves with it. + // The embedded edge is self-interior (not an object base) — + // the evacuation fixup rebases it instead. + if (!shaped_slots_are_embedded (obj)) + scan_func (&(obj->slots)); + } + scan_func (&(obj->proto)); + return; + } + scan_property_entries (obj->map, scan_func); + scan_func (&(obj->proto)); } // ECMA262: 9.1.3 [[IsExtensible]] ( ) @@ -2469,16 +3066,43 @@ _ejs_object_specop_own_property_keys (ejsval O) { EJSObject* O_ = EJSVAL_TO_OBJECT(O); - ejsval* numberkeys = malloc(sizeof(ejsval) *O_->map->inuse); + // shaped mode: snapshot the own property names from whichever store + // this object uses; the classification below is shared so the two + // modes stay byte-identical + uint32_t O_shape = EJS_OBJECT_SHAPE(O_); + int nprops; + ejsval shaped_names[256]; + if (O_shape != EJS_SHAPE_DICT) { + nprops = (int)_ejs_shape_field_count(O_shape); + _ejs_shape_fields (O_shape, shaped_names); + } + else { + nprops = O_->map->inuse; + } + + ejsval* numberkeys = malloc(sizeof(ejsval) * nprops); int num_numberkeys = 0; - ejsval* stringkeys = malloc(sizeof(ejsval) *O_->map->inuse); + ejsval* stringkeys = malloc(sizeof(ejsval) * nprops); int num_stringkeys = 0; - ejsval* symbolkeys = malloc(sizeof(ejsval) *O_->map->inuse); + ejsval* symbolkeys = malloc(sizeof(ejsval) * nprops); int num_symbolkeys = 0; - // 1. Let keys be a new empty List. - for (_EJSPropertyMapEntry *s = O_->map->head_insert; s; s = s->next_insert) { - if (EJSVAL_IS_STRING(s->name)) { - ejsval idx_val = ToNumber(s->name); + // 1. Let keys be a new empty List. + _EJSPropertyMapEntry *s = O_shape == EJS_SHAPE_DICT ? O_->map->head_insert : NULL; + for (int i = 0; ; i ++) { + ejsval name; + if (O_shape != EJS_SHAPE_DICT) { + if (i >= nprops) + break; + name = shaped_names[i]; + } + else { + if (!s) + break; + name = s->name; + s = s->next_insert; + } + if (EJSVAL_IS_STRING(name)) { + ejsval idx_val = ToNumber(name); if (EJSVAL_IS_NUMBER(idx_val)) { double n = EJSVAL_TO_NUMBER(idx_val); if (n >= 0 && floor(n) == n) { @@ -2486,18 +3110,18 @@ _ejs_object_specop_own_property_keys (ejsval O) // a. Add P as the last element of keys. // we just append them as we do strings/symbols below. we'll sort after our pass over the map - numberkeys[num_numberkeys++] = s->name; + numberkeys[num_numberkeys++] = name; continue; } } - // 3. For each own property key P of O that is a String but is not an integer index, in property creation order - // a. Add P as the last element of keys. - stringkeys[num_stringkeys++] = s->name; + // 3. For each own property key P of O that is a String but is not an integer index, in property creation order + // a. Add P as the last element of keys. + stringkeys[num_stringkeys++] = name; } else { // 4. For each own property key P of O that is a Symbol, in property creation order - // a. Add P as the last element of keys. - symbolkeys[num_symbolkeys++] = s->name; + // a. Add P as the last element of keys. + symbolkeys[num_symbolkeys++] = name; } } diff --git a/runtime/ejs-object.h b/runtime/ejs-object.h index db0e8a2e..965b7e51 100644 --- a/runtime/ejs-object.h +++ b/runtime/ejs-object.h @@ -230,7 +230,15 @@ struct _EJSObject { GCObjectHeader gc_header; EJSSpecOps* ops; ejsval proto; // [[Prototype]] - EJSPropertyMap* map; + // property storage is mode-switched on the + // header's shape index. Dictionary mode (shape 0) keeps the map; + // shaped mode stores plain data property values in a closureenv + // slot array (an ejsval so the GC scan traces it; _ejs_null until + // the first property arrives) at shape-determined indices. + union { + EJSPropertyMap* map; // dictionary mode + ejsval slots; // shaped mode + }; }; @@ -249,6 +257,7 @@ void _ejs_propertymap_foreach_property (EJSPropertyMap *map, EJSPropertyDescFunc EJSBool _ejs_object_define_value_property (ejsval obj, ejsval key, ejsval value, uint32_t flags); EJSBool _ejs_object_define_accessor_property (ejsval obj, ejsval key, ejsval get, ejsval set, uint32_t flags); +EJSBool _ejs_object_define_accessor_property_desc (ejsval obj, ejsval key, ejsval get, ejsval set, uint32_t flags); ejsval _ejs_object_setprop (ejsval obj, ejsval key, ejsval value); ejsval _ejs_object_getprop (ejsval obj, ejsval key); @@ -271,6 +280,16 @@ extern ejsval _ejs_Object__proto__; extern ejsval _ejs_Object_prototype; extern EJSSpecOps _ejs_Object_specops; +// the accessor epoch: 0 while no user code has installed anything that +// could intercept a [[Set]] on a fresh object's prototype chain — an +// accessor property, a non-writable data property, or a prototype swap. +// Compiled construct sites test `== 0` to run virtualized (allocation- +// free) constructor results; every intercept-capable installation +// retires that fast path process-wide by bumping the counter. Builtin +// init installs (e.g. Object.prototype.__proto__) predate the zeroing +// at the end of _ejs_init, so they never count. See docs/sinking-plan.md. +extern uint64_t _ejs_accessor_epoch; + void _ejs_object_init_proto(); ejsval _ejs_object_new (ejsval proto, EJSSpecOps* ops); @@ -285,9 +304,25 @@ extern EJS_NATIVE_FUNC(_ejs_Object_prototype_toString); // exposed so we can call the native implementation during class creation ejsval _ejs_object_set_prototype_of (ejsval obj, ejsval proto); +ejsval _ejs_object_literal_set_proto (ejsval obj, ejsval proto); ejsval _ejs_object_create (ejsval proto); +// born-with-shape: batch a statically-keyed literal's +// (new_shaped) or a fenced constructor prefix's (fill_shaped) field +// installs into one call. names are interned atoms and values the +// initial field values, in source order; both fall back to sequential +// generic sets whenever the shaped fast path doesn't apply, so behavior +// is identical to the unbatched lowering (incl. EJS_SHAPES=off). +ejsval _ejs_object_new_shaped (uint32_t argc, ejsval* names, ejsval* values); +ejsval _ejs_object_fill_shaped (ejsval obj, uint32_t argc, ejsval* names, ejsval* values); + +// ordinary-construct support (gc-P5): allocate an empty root-shaped +// ordinary object whose slot storage for `hint` fields is embedded in +// the object's own cell (0 = bare object, today's layout). Constructor +// birth-capacity hints route here so `new F()` results are single-cell. +ejsval _ejs_object_new_with_slot_hint (ejsval proto, uint32_t hint); + void _ejs_Object_init (ejsval ejs_global); EJS_END_DECLS diff --git a/runtime/ejs-ops.c b/runtime/ejs-ops.c index bbd5ff0a..09d01e17 100644 --- a/runtime/ejs-ops.c +++ b/runtime/ejs-ops.c @@ -2,6 +2,7 @@ * vim: set ts=4 sw=4 et tw=99 ft=cpp: */ +#include #include #include #include @@ -205,28 +206,101 @@ ejsval ToString(ejsval exp) EJS_NOT_IMPLEMENTED(); } +// ES WhiteSpace ∪ LineTerminator (the code points StringToNumber strips) +static EJSBool +is_js_whitespace(jschar c) +{ + switch (c) { + case 0x09: case 0x0A: case 0x0B: case 0x0C: case 0x0D: case 0x20: + case 0xA0: case 0x1680: case 0x2028: case 0x2029: case 0x202F: + case 0x205F: case 0x3000: case 0xFEFF: + return EJS_TRUE; + default: + return (c >= 0x2000 && c <= 0x200A); + } +} + +// ES 7.1.3.1 StringToNumber, on the whitespace-trimmed code units. +// strtod accepts spellings the StrNumericLiteral grammar doesn't +// ("inf", "nan", hex floats), so those are screened out up front. +static double +StringToNumber(const jschar* chars, int32_t len) +{ + while (len > 0 && is_js_whitespace(*chars)) { chars++; len--; } + while (len > 0 && is_js_whitespace(chars[len-1])) len--; + + if (len == 0) + return 0; + + char buf[128]; + char* num_utf8 = buf; + if (len + 1 > (int32_t)sizeof(buf)) + num_utf8 = (char*)malloc(len + 1); + // NaN on any non-ASCII code unit: every StrNumericLiteral is ASCII + for (int32_t i = 0; i < len; i++) { + if (chars[i] > 0x7f) { + if (num_utf8 != buf) free(num_utf8); + return nan(""); + } + num_utf8[i] = (char)chars[i]; + } + num_utf8[len] = 0; + + double d; + const char* body = num_utf8; + double sign = 1; + if (*body == '+' || *body == '-') { + if (*body == '-') sign = -1; + body++; + } + if ((body[0] == 'i' || body[0] == 'I') || (body[0] == 'n' || body[0] == 'N')) { + // of strtod's inf/nan spellings only exactly "Infinity" is a + // StrNumericLiteral + d = !strcmp(body, "Infinity") ? sign * INFINITY : nan(""); + } + else if (body[0] == '0' && (body[1] == 'b' || body[1] == 'B' || + body[1] == 'o' || body[1] == 'O')) { + // ES6 binary/octal literals (sign is not part of the grammar) + int base = (body[1] == 'b' || body[1] == 'B') ? 2 : 8; + d = (sign == 1 && body[2] != 0) ? 0 : nan(""); + for (const char* p = body + 2; *p && !isnan(d); p++) { + int digit = *p - '0'; + d = (digit >= 0 && digit < base) ? d * base + digit : nan(""); + } + } + else { + if (body[0] == '0' && (body[1] == 'x' || body[1] == 'X')) { + // strtod would also take a hex-float exponent, and a sign + // isn't part of the grammar + EJSBool ok = sign == 1 && body[2] != 0; + for (const char* p = body + 2; ok && *p; p++) + if (!isxdigit((unsigned char)*p)) ok = EJS_FALSE; + if (!ok) { + if (num_utf8 != buf) free(num_utf8); + return nan(""); + } + } + char* endptr; + d = strtod(num_utf8, &endptr); + if (*endptr != '\0') + d = nan(""); + } + + if (num_utf8 != buf) free(num_utf8); + return d; +} + ejsval ToNumber(ejsval exp) { if (EJSVAL_IS_NUMBER(exp)) return exp; else if (EJSVAL_IS_BOOLEAN(exp)) return EJSVAL_TO_BOOLEAN(exp) ? _ejs_one : _ejs_zero; + else if (EJSVAL_IS_NULL(exp)) + return _ejs_zero; else if (EJSVAL_IS_STRING(exp)) { - char num_utf8_buf[128]; - memset(num_utf8_buf, 0, sizeof(num_utf8_buf)); - char* num_utf8 = ucs2_to_utf8_buf(EJSVAL_TO_FLAT_STRING(exp), num_utf8_buf, sizeof(num_utf8_buf)); - if (num_utf8 == NULL) { - num_utf8 = ucs2_to_utf8(EJSVAL_TO_FLAT_STRING(exp)); - } - char *endptr; - double d = strtod(num_utf8, &endptr); - if (*endptr != '\0') { - if (num_utf8 != num_utf8_buf) free (num_utf8); - return _ejs_nan; - } - ejsval rv = NUMBER_TO_EJSVAL(d); // XXX NaN - if (num_utf8 != num_utf8_buf) free (num_utf8); - return rv; + EJSPrimString* flat = _ejs_string_flatten(exp); + return NUMBER_TO_EJSVAL(StringToNumber(flat->data.flat, flat->length)); } else if (EJSVAL_IS_SYMBOL(exp)) { _ejs_throw_nativeerror_utf8 (EJS_TYPE_ERROR, "1"); // XXX @@ -328,14 +402,14 @@ int64_t ToLength(ejsval exp) uint32_t ToUint32(ejsval exp) { - // XXX sorely lacking - return (uint32_t)ToDouble(exp); + // same modulo-2^32 wrap as ToInt32, reinterpreted unsigned + // (casting a negative double straight to uint32_t is UB) + return (uint32_t)ToInt32(exp); } uint16_t ToUint16(ejsval exp) { - // XXX sorely lacking - return (uint16_t)ToDouble(exp); + return (uint16_t)ToInt32(exp); } ejsval ToObject(ejsval exp) @@ -481,7 +555,11 @@ SameValue(ejsval x, ejsval y) // 2. ReturnIfAbrupt(y). // 3. If Type(x) is different from Type(y), return false. - if (EJSVAL_TO_TAG(x) != EJSVAL_TO_TAG(y)) return EJS_FALSE; + // (numbers checked apart from the tag compare: ±0 and NaNs with + // different payloads carry different NaN-box tags but are the + // same Type) + if (EJSVAL_IS_NUMBER(x) != EJSVAL_IS_NUMBER(y)) return EJS_FALSE; + if (!EJSVAL_IS_NUMBER(x) && EJSVAL_TO_TAG(x) != EJSVAL_TO_TAG(y)) return EJS_FALSE; // 4. If Type(x) is Undefined, return true. if (EJSVAL_IS_UNDEFINED(x)) return EJS_TRUE; @@ -491,16 +569,16 @@ SameValue(ejsval x, ejsval y) // 6. If Type(x) is Number, then if (EJSVAL_IS_NUMBER(x)) { + double dx = EJSVAL_TO_NUMBER(x); + double dy = EJSVAL_TO_NUMBER(y); // a. If x is NaN and y is NaN, return true. - if (isnan(EJSVAL_TO_NUMBER(x)) && isnan(EJSVAL_TO_NUMBER(y))) return EJS_TRUE; - // b. If x is +0 and y is -0, return false. - if (EJSVAL_TO_NUMBER(x) == 0.0 && EJSDOUBLE_IS_NEGZERO(EJSVAL_TO_NUMBER(y))) return EJS_FALSE; - // c. If x is -0 and y is +0, return false. - if (EJSDOUBLE_IS_NEGZERO(EJSVAL_TO_NUMBER(x)) == 0.0 && EJSVAL_TO_NUMBER(y) == 0) return EJS_FALSE; + if (isnan(dx) && isnan(dy)) return EJS_TRUE; + // b/c. +0 and -0 are different values. + if (dx == 0 && dy == 0) + return EJSDOUBLE_IS_NEGZERO(dx) == EJSDOUBLE_IS_NEGZERO(dy); // d. If x is the same Number value as y, return true. - if (EJSVAL_TO_NUMBER(x) == EJSVAL_TO_NUMBER(y)) return EJS_TRUE; // e. Return false. - return EJS_FALSE; + return dx == dy ? EJS_TRUE : EJS_FALSE; } // 7. If Type(x) is String, then if (EJSVAL_IS_STRING(x)) { @@ -536,9 +614,9 @@ SameValueZero(ejsval x, ejsval y) // 2. ReturnIfAbrupt(y). // 3. If Type(x) is different from Type(y), return false. - if ((EJSVAL_IS_NUMBER(x) != EJSVAL_IS_NUMBER(y)) && - (EJSVAL_TO_TAG(x) != EJSVAL_TO_TAG(y))) - return EJS_FALSE; + // (numbers checked apart from the tag compare, as in SameValue) + if (EJSVAL_IS_NUMBER(x) != EJSVAL_IS_NUMBER(y)) return EJS_FALSE; + if (!EJSVAL_IS_NUMBER(x) && EJSVAL_TO_TAG(x) != EJSVAL_TO_TAG(y)) return EJS_FALSE; // 4. If Type(x) is Undefined, return true. if (EJSVAL_IS_UNDEFINED(x)) return EJS_TRUE; @@ -548,16 +626,13 @@ SameValueZero(ejsval x, ejsval y) // 6. If Type(x) is Number, then if (EJSVAL_IS_NUMBER(x)) { + double dx = EJSVAL_TO_NUMBER(x); + double dy = EJSVAL_TO_NUMBER(y); // a. If x is NaN and y is NaN, return true. - if (isnan(EJSVAL_TO_NUMBER(x)) && isnan(EJSVAL_TO_NUMBER(y))) return EJS_TRUE; - // b. If x is +0 and y is -0, return true. - if (EJSVAL_TO_NUMBER(x) == 0.0 && EJSDOUBLE_IS_NEGZERO(EJSVAL_TO_NUMBER(y))) return EJS_TRUE; - // c. If x is -0 and y is +0, return true. - if (EJSDOUBLE_IS_NEGZERO(EJSVAL_TO_NUMBER(x)) && EJSVAL_TO_NUMBER(y) == 0) return EJS_TRUE; - // d. If x is the same Number value as y, return true. - if (EJSVAL_TO_NUMBER(x) == EJSVAL_TO_NUMBER(y)) return EJS_TRUE; + if (isnan(dx) && isnan(dy)) return EJS_TRUE; + // b/c/d. IEEE == : ±0 equal, same value equal. // e. Return false. - return EJS_FALSE; + return dx == dy ? EJS_TRUE : EJS_FALSE; } // 7. If Type(x) is String, then if (EJSVAL_IS_STRING(x)) { @@ -604,7 +679,7 @@ _ejs_op_not (ejsval exp) ejsval _ejs_op_bitwise_not (ejsval val) { - int val_int = ToInteger(val); + int32_t val_int = ToInt32(val); return NUMBER_TO_EJSVAL (~val_int); } @@ -617,7 +692,9 @@ _ejs_op_void (ejsval exp) ejsval _ejs_op_typeof_is_object(ejsval exp) { - return EJSVAL_IS_OBJECT(exp) ? _ejs_true : _ejs_false; + // must match _ejs_op_typeof: functions are "function", null is "object" + if (EJSVAL_IS_NULL(exp)) return _ejs_true; + return (EJSVAL_IS_OBJECT(exp) && !EJSVAL_IS_FUNCTION(exp)) ? _ejs_true : _ejs_false; } ejsval @@ -659,7 +736,8 @@ _ejs_op_typeof_is_boolean(ejsval exp) ejsval _ejs_op_typeof_is_null(ejsval exp) { - return EJSVAL_IS_NULL(exp) ? _ejs_true : _ejs_false; + // typeof never evaluates to "null" (typeof null is "object") + return _ejs_false; } int @@ -673,7 +751,7 @@ ejsval _ejs_op_typeof (ejsval exp) { if (EJSVAL_IS_NULL(exp)) - return _ejs_atom_null; + return _ejs_atom_object; else if (EJSVAL_IS_BOOLEAN(exp)) return _ejs_atom_boolean; else if (EJSVAL_IS_STRING(exp)) @@ -707,25 +785,9 @@ _ejs_op_delete (ejsval obj, ejsval prop) ejsval _ejs_op_mod (ejsval lhs, ejsval rhs) { - if (EJSVAL_IS_NUMBER(lhs)) { - if (EJSVAL_IS_NUMBER(rhs)) { - return NUMBER_TO_EJSVAL (fmod(EJSVAL_TO_NUMBER(lhs), EJSVAL_TO_NUMBER(rhs))); - } - else { - // need to call valueOf() on the object, or convert the string to a number - EJS_NOT_IMPLEMENTED(); - } - } - else if (EJSVAL_IS_STRING(lhs)) { - // string+ with anything we don't implement yet - it will call toString() on objects, and convert a number to a string - EJS_NOT_IMPLEMENTED(); - } - else { - // object+... how does js implement this anyway? - EJS_NOT_IMPLEMENTED(); - } - - return _ejs_nan; + double ld = ToDouble(lhs); + double rd = ToDouble(rhs); + return NUMBER_TO_EJSVAL (fmod(ld, rd)); } ejsval @@ -739,113 +801,41 @@ _ejs_op_bitwise_xor (ejsval lhs, ejsval rhs) ejsval _ejs_op_bitwise_and (ejsval lhs, ejsval rhs) { - int lhs_int = ToInteger(lhs); - int rhs_int = ToInteger(rhs); + int32_t lhs_int = ToInt32(lhs); + int32_t rhs_int = ToInt32(rhs); return NUMBER_TO_EJSVAL (lhs_int & rhs_int); } ejsval _ejs_op_bitwise_or (ejsval lhs, ejsval rhs) { - int lhs_int = ToInteger(lhs); - int rhs_int = ToInteger(rhs); + int32_t lhs_int = ToInt32(lhs); + int32_t rhs_int = ToInt32(rhs); return NUMBER_TO_EJSVAL (lhs_int | rhs_int); } ejsval _ejs_op_rsh (ejsval lhs, ejsval rhs) { - if (EJSVAL_IS_NUMBER(lhs)) { - if (EJSVAL_IS_NUMBER(rhs)) { - return NUMBER_TO_EJSVAL ((int)((int)EJSVAL_TO_NUMBER(lhs) >> (((unsigned int)EJSVAL_TO_NUMBER(rhs)) & 0x1f))); - } - else { - // need to call valueOf() on the object, or convert the string to a number - EJS_NOT_IMPLEMENTED(); - } - } - else if (EJSVAL_IS_STRING(lhs)) { - // string+ with anything we don't implement yet - it will call toString() on objects, and convert a number to a string - EJS_NOT_IMPLEMENTED(); - } - else { - // object+... how does js implement this anyway? - EJS_NOT_IMPLEMENTED(); - } - - return _ejs_nan; + return NUMBER_TO_EJSVAL (ToInt32(lhs) >> (ToUint32(rhs) & 0x1f)); } ejsval _ejs_op_ursh (ejsval lhs, ejsval rhs) { - if (EJSVAL_IS_NUMBER(lhs)) { - if (EJSVAL_IS_NUMBER(rhs)) { - return NUMBER_TO_EJSVAL ((unsigned int)((unsigned int)EJSVAL_TO_NUMBER(lhs) >> (((unsigned int)EJSVAL_TO_NUMBER(rhs)) & 0x1f))); - } - else { - // need to call valueOf() on the object, or convert the string to a number - EJS_NOT_IMPLEMENTED(); - } - } - else if (EJSVAL_IS_STRING(lhs)) { - // string+ with anything we don't implement yet - it will call toString() on objects, and convert a number to a string - EJS_NOT_IMPLEMENTED(); - } - else { - // object+... how does js implement this anyway? - EJS_NOT_IMPLEMENTED(); - } - - return _ejs_nan; + return NUMBER_TO_EJSVAL (ToUint32(lhs) >> (ToUint32(rhs) & 0x1f)); } ejsval _ejs_op_lsh (ejsval lhs, ejsval rhs) { - if (EJSVAL_IS_NUMBER(lhs)) { - if (EJSVAL_IS_NUMBER(rhs)) { - return NUMBER_TO_EJSVAL ((int)((int)EJSVAL_TO_NUMBER(lhs) << (((unsigned int)EJSVAL_TO_NUMBER(rhs)) & 0x1f))); - } - else { - // need to call valueOf() on the object, or convert the string to a number - EJS_NOT_IMPLEMENTED(); - } - } - else if (EJSVAL_IS_STRING(lhs)) { - // string+ with anything we don't implement yet - it will call toString() on objects, and convert a number to a string - EJS_NOT_IMPLEMENTED(); - } - else { - // object+... how does js implement this anyway? - EJS_NOT_IMPLEMENTED(); - } - - return _ejs_nan; + return NUMBER_TO_EJSVAL ((int32_t)((uint32_t)ToInt32(lhs) << (ToUint32(rhs) & 0x1f))); } ejsval _ejs_op_ulsh (ejsval lhs, ejsval rhs) { - if (EJSVAL_IS_NUMBER(lhs)) { - if (EJSVAL_IS_NUMBER(rhs)) { - return NUMBER_TO_EJSVAL ((unsigned int)((unsigned int)EJSVAL_TO_NUMBER(lhs) << (((unsigned int)EJSVAL_TO_NUMBER(rhs)) & 0x1f))); - } - else { - // need to call valueOf() on the object, or convert the string to a number - EJS_NOT_IMPLEMENTED(); - } - } - else if (EJSVAL_IS_STRING(lhs)) { - // string+ with anything we don't implement yet - it will call toString() on objects, and convert a number to a string - EJS_NOT_IMPLEMENTED(); - } - else { - // object+... how does js implement this anyway? - EJS_NOT_IMPLEMENTED(); - } - - return _ejs_nan; + return NUMBER_TO_EJSVAL (ToUint32(lhs) << (ToUint32(rhs) & 0x1f)); } ejsval @@ -858,9 +848,11 @@ _ejs_op_add (ejsval lhs, ejsval rhs) lprim = ToPrimitive(lhs, TO_PRIM_HINT_DEFAULT); rprim = ToPrimitive(rhs, TO_PRIM_HINT_DEFAULT); - if (EJSVAL_IS_STRING(lhs) || EJSVAL_IS_STRING(rhs)) { - ejsval lhstring = ToString(lhs); - ejsval rhstring = ToString(rhs); + // ES: the string test is on the ToPrimitive results (an object + // whose primitive is a string still concatenates) + if (EJSVAL_IS_STRING(lprim) || EJSVAL_IS_STRING(rprim)) { + ejsval lhstring = ToString(lprim); + ejsval rhstring = ToString(rprim); ejsval result = _ejs_string_concat (lhstring, rhstring); rv = result; @@ -875,37 +867,17 @@ _ejs_op_add (ejsval lhs, ejsval rhs) ejsval _ejs_op_mult (ejsval lhs, ejsval rhs) { - if (EJSVAL_IS_NUMBER(lhs) || EJSVAL_IS_NUMBER(rhs)) { - return NUMBER_TO_EJSVAL (ToDouble(lhs) * ToDouble(rhs)); - } - else if (EJSVAL_IS_STRING(lhs)) { - // string+ with anything we don't implement yet - it will call toString() on objects, and convert a number to a string - EJS_NOT_IMPLEMENTED(); - } - else { - // object+... how does js implement this anyway? - EJS_NOT_IMPLEMENTED(); - } - - return _ejs_nan; + double ld = ToDouble(lhs); + double rd = ToDouble(rhs); + return NUMBER_TO_EJSVAL (ld * rd); } ejsval _ejs_op_div (ejsval lhs, ejsval rhs) { - if (EJSVAL_IS_NUMBER(lhs)) { - return NUMBER_TO_EJSVAL (EJSVAL_TO_NUMBER(lhs) / ToDouble (rhs)); - } - else if (EJSVAL_IS_STRING(lhs)) { - // string+ with anything we don't implement yet - it will call toString() on objects, and convert a number to a string - EJS_NOT_IMPLEMENTED(); - } - else { - // object+... how does js implement this anyway? - EJS_NOT_IMPLEMENTED(); - } - - return _ejs_nan; + double ld = ToDouble(lhs); + double rd = ToDouble(rhs); + return NUMBER_TO_EJSVAL (ld / rd); } ejsval @@ -989,7 +961,9 @@ _ejs_op_ge (ejsval lhs, ejsval rhs) ejsval _ejs_op_sub (ejsval lhs, ejsval rhs) { - return NUMBER_TO_EJSVAL(ToDouble(lhs) - ToDouble(rhs)); + double ld = ToDouble(lhs); + double rd = ToDouble(rhs); + return NUMBER_TO_EJSVAL(ld - rd); } // ECMA262 7.2.13 @@ -997,34 +971,22 @@ _ejs_op_sub (ejsval lhs, ejsval rhs) ejsval _ejs_op_strict_eq (ejsval x, ejsval y) { + // Numbers first: a NaN-box tag compare can't see that -0 and +0 + // (different bit patterns) are the same Number value. IEEE == + // handles NaN (false) and ±0 (true) exactly per the spec. + if (EJSVAL_IS_NUMBER(x)) { + if (!EJSVAL_IS_NUMBER(y)) return _ejs_false; + return EJSVAL_TO_NUMBER(x) == EJSVAL_TO_NUMBER(y) ? _ejs_true : _ejs_false; + } + // 1. If Type(x) is different from Type(y), return false. if (EJSVAL_TO_TAG(x) != EJSVAL_TO_TAG(y)) return _ejs_false; - + // 2. If Type(x) is Undefined, return true. if (EJSVAL_IS_UNDEFINED(x)) return _ejs_true; // 3. If Type(x) is Null, return true. if (EJSVAL_IS_NULL(x)) return _ejs_true; - - // 4. If Type(x) is Number, then - if (EJSVAL_IS_NUMBER(x)) { - // a. If x is NaN, return false. - if (isnan(EJSVAL_TO_NUMBER(x))) return _ejs_false; - - // b. If y is NaN, return false. - if (isnan(EJSVAL_TO_NUMBER(y))) return _ejs_false; - - // c. If x is the same Number value as y, return true. - if (EJSVAL_TO_NUMBER(x) == EJSVAL_TO_NUMBER(y)) return _ejs_true; - - // d. If x is +0 and y is -0, return true. - if (EJSVAL_TO_NUMBER(x) == 0.0 && EJSDOUBLE_IS_NEGZERO(EJSVAL_TO_NUMBER(y))) return _ejs_true; - // e. If x is -0 and y is +0, return true. - if (EJSDOUBLE_IS_NEGZERO(EJSVAL_TO_NUMBER(x)) == 0.0 && EJSVAL_TO_NUMBER(y) == 0) return _ejs_true; - - // f. Return false. - return _ejs_false; - } // 5. If Type(x) is String, then if (EJSVAL_IS_STRING(x)) { // a. If x and y are exactly the same sequence of characters (same length and same characters in corresponding positions), return true. @@ -1062,7 +1024,9 @@ _ejs_op_eq (ejsval x, ejsval y) // 1. ReturnIfAbrupt(x). // 2. ReturnIfAbrupt(y). // 3. If Type(x) is the same as Type(y), then - if (EJSVAL_TO_TAG(x) == EJSVAL_TO_TAG(y)) + // (numbers checked apart from the tag compare: ±0 carry + // different NaN-box tags but are the same Type) + if ((EJSVAL_IS_NUMBER(x) && EJSVAL_IS_NUMBER(y)) || EJSVAL_TO_TAG(x) == EJSVAL_TO_TAG(y)) // a. Return the result of performing Strict Equality Comparison x === y. return _ejs_op_strict_eq(x, y); // 4. If x is null and y is undefined, return true. @@ -1329,7 +1293,12 @@ EJS_NATIVE_FUNC(_ejs_parseInt_impl) { /* implementation; and if R is not 2, 4, 8, 10, 16, or 32, then mathInt may be an implementation-dependent */ /* approximation to the mathematical integer value that is represented by Z in radix-R notation.) */ - int mathInt = 0; + // the accumulator must be a double: js numbers aren't int32, and e.g. + // parseInt("ffffffff", 16) must be 4294967295, not -1. (esprima uses + // parseInt for hex literals, so this int32 overflow made the compiled + // compiler read 0xffffffff literals as -1 and emit corrupt nanboxing + // masks.) + double mathInt = 0; int32_t Zlen = i; for (i = 0; i < Zlen; i ++) { jschar needle[2]; @@ -1349,7 +1318,7 @@ EJS_NATIVE_FUNC(_ejs_parseInt_impl) { } /* 14. Let number be the Number value for mathInt. */ - int32_t number = mathInt * sign; + double number = mathInt * sign; /* 15. Return sign * number */ return NUMBER_TO_EJSVAL(number); diff --git a/runtime/ejs-promise.c b/runtime/ejs-promise.c index 53ebdcbd..b03fe447 100644 --- a/runtime/ejs-promise.c +++ b/runtime/ejs-promise.c @@ -127,6 +127,7 @@ static ejsval RejectPromise (ejsval promise, ejsval reason) // 3. Set the value of promise's [[PromiseResult]] internal slot to reason. _promise->result = reason; + _ejs_gc_remember(_promise, _promise->result); // 4. Set the value of promise's [[PromiseFulfillReactions]] internal slot to undefined. // XXX we need to free our listnodes @@ -158,6 +159,7 @@ static ejsval FulfillPromise (ejsval promise, ejsval resolutionValue) EJSPromiseReaction* reactions = _promise->fulfillReactions; // 3. Set the value of promise's [[PromiseResult]] internal slot to resolutionvalue. _promise->result = resolutionValue; + _ejs_gc_remember(_promise, _promise->result); // 4. Set the value of promise's [[PromiseFulfullReactions]] internal slot to undefined. // XXX we need to free our listnodes @@ -288,6 +290,7 @@ CreateResolvingFunctions(ejsval promise, ejsval* out_resolve, ejsval* out_reject ejsval resolvingFunctions_env = _ejs_closureenv_new(2); *_ejs_closureenv_get_slot_ref(resolvingFunctions_env, 0) = _ejs_false; *_ejs_closureenv_get_slot_ref(resolvingFunctions_env, 1) = promise; + EJS_GC_REMEMBER(resolvingFunctions_env, promise); // 2. Let resolve be a new built-in function object as defined in Promise Resolve Functions (25.4.1.4). // 3. Set the [[Promise]] internal slot of resolve to promise. @@ -412,14 +415,14 @@ PromiseReactionTask (EJSPromiseReaction* reaction, ejsval argument) handlerResult = argument; } // 5. Else If handler is "Thrower", then let handlerResult be Completion{[[type]]: throw, [[value]]: argument, [[target]]: empty}. - if (SameValue(handler, _ejs_thrower_function)) { + else if (SameValue(handler, _ejs_thrower_function)) { success = EJS_FALSE; handlerResult = argument; } // 6. Else, Let let handlerResult be the result of calling the [[Call]] internal method of handler passing undefined as thisArgument and (argument) as argumentsList. else { ejsval undef_this = _ejs_undefined; - success = _ejs_invoke_closure_catch(&handlerResult, handler, &undef_this, 1, &argument, _ejs_undefined); + success = _ejs_invoke_closure_catch(&handlerResult, handler, &undef_this, 1, &argument, _ejs_undefined); } ejsval status; @@ -428,7 +431,7 @@ PromiseReactionTask (EJSPromiseReaction* reaction, ejsval argument) if (!success) { ejsval undef_this = _ejs_undefined; // a. Let status be the result of calling the [[Call]] internal method of promiseCapability.[[Reject]] passing undefined as thisArgument and (handlerResult.[[value]]) as argumentsList. - success = _ejs_invoke_closure_catch(&status, EJS_CAPABILITY_GET_REJECT(promiseCapability), &undef_this, 1, &handlerResult, _ejs_undefined); + /* notyet success = */ _ejs_invoke_closure_catch(&status, EJS_CAPABILITY_GET_REJECT(promiseCapability), &undef_this, 1, &handlerResult, _ejs_undefined); // b. NextTask status. return;//EJS_NOT_IMPLEMENTED(); @@ -436,7 +439,7 @@ PromiseReactionTask (EJSPromiseReaction* reaction, ejsval argument) // 8. Let handlerResult be handlerResult.[[value]]. // 9. Let status be the result of calling the [[Call]] internal method of promiseCapability.[[Resolve]] passing undefined as thisArgument and (handlerResult) as argumentsList. ejsval undef_this = _ejs_undefined; - success = _ejs_invoke_closure_catch(&status, EJS_CAPABILITY_GET_RESOLVE(promiseCapability), &undef_this, 1, &handlerResult, _ejs_undefined); + /* notyet success = */ _ejs_invoke_closure_catch(&status, EJS_CAPABILITY_GET_RESOLVE(promiseCapability), &undef_this, 1, &handlerResult, _ejs_undefined); // 10. NextTask status. } @@ -638,6 +641,7 @@ static EJS_NATIVE_FUNC(resolve_element) { // 2. Set the value of F's [[AlreadyCalled]] internal slot to true. EJS_RESOLVEELEMENT_SET_ALREADY_CALLED(env, _ejs_true); +#if notyet // 3. Let index be the value of F's [[Index]] internal slot. ejsval index = EJS_RESOLVEELEMENT_GET_INDEX(env); @@ -646,6 +650,7 @@ static EJS_NATIVE_FUNC(resolve_element) { // 5. Let promiseCapability be the value of F's [[Capabilities]] internal slot. ejsval promiseCapability = EJS_RESOLVEELEMENT_GET_CAPABILITIES(env); +#endif // 6. Let remainingElementsCount be the value of F's [[RemainingElements]] internal slot. @@ -955,17 +960,17 @@ _ejs_promise_specop_scan (EJSObject* obj, EJSValueFunc scan_func) { EJSPromise* promise = (EJSPromise*)obj; - scan_func(promise->result); - scan_func(promise->constructor); + scan_func(&(promise->result)); + scan_func(&(promise->constructor)); for (EJSPromiseReaction* reaction = promise->fulfillReactions; reaction; reaction = reaction->next) { - scan_func(reaction->capabilities); - scan_func(reaction->handler); + scan_func(&(reaction->capabilities)); + scan_func(&(reaction->handler)); } for (EJSPromiseReaction* reaction = promise->rejectReactions; reaction; reaction = reaction->next) { - scan_func(reaction->capabilities); - scan_func(reaction->handler); + scan_func(&(reaction->capabilities)); + scan_func(&(reaction->handler)); } _ejs_Object_specops.Scan (obj, scan_func); diff --git a/runtime/ejs-proxy.c b/runtime/ejs-proxy.c index e286aa99..28d9b02c 100644 --- a/runtime/ejs-proxy.c +++ b/runtime/ejs-proxy.c @@ -160,6 +160,8 @@ _ejs_proxy_specop_get_prototype_of (ejsval O) static EJSBool _ejs_proxy_specop_set_prototype_of (ejsval O, ejsval V) { + // trapped proto swaps never reach the ordinary specop's bump + _ejs_accessor_epoch++; EJSProxy* proxy = EJSVAL_TO_PROXY(O); // 1. Assert: Either Type(V) is Object or Type(V) is Null. @@ -428,7 +430,9 @@ _ejs_proxy_specop_get_own_property (ejsval O, ejsval P, ejsval* exc) // 14. Let extensibleTarget be IsExtensible(target). // 15. ReturnIfAbrupt(extensibleTarget). +#if notyet EJSBool extensibleTarget = EJS_OBJECT_IS_EXTENSIBLE(_target); +#endif // 16. Let resultDesc be ToPropertyDescriptor(trapResultObj). // 17. ReturnIfAbrupt(resultDesc). @@ -836,8 +840,8 @@ static void _ejs_proxy_specop_scan (EJSObject* obj, EJSValueFunc scan_func) { EJSProxy* proxy = (EJSProxy*)obj; - scan_func(proxy->target); - scan_func(proxy->handler); + scan_func(&(proxy->target)); + scan_func(&(proxy->handler)); _ejs_Object_specops.Scan (obj, scan_func); } diff --git a/runtime/ejs-regexp.c b/runtime/ejs-regexp.c index 660e437f..8269ad01 100644 --- a/runtime/ejs-regexp.c +++ b/runtime/ejs-regexp.c @@ -15,7 +15,7 @@ #include "ejs-proxy.h" #include "ejs-number.h" -#include "pcre.h" +#include "external-deps/pcre/pcre.h" ejsval _ejs_RegExp_prototype_exec_closure; @@ -227,8 +227,11 @@ RegExpInitialize(ejsval obj, ejsval pattern, ejsval flags) { const char *pcre_error; int pcre_erroffset; + int pcre_options = PCRE_UTF16 | PCRE_NO_UTF16_CHECK; + if (re->ignoreCase) pcre_options |= PCRE_CASELESS; + if (re->multiline) pcre_options |= PCRE_MULTILINE; re->compiled_pattern = pcre16_compile(chars, - PCRE_UTF16 | PCRE_NO_UTF16_CHECK, + pcre_options, &pcre_error, &pcre_erroffset, pcre16_tables); @@ -1205,8 +1208,8 @@ static void _ejs_regexp_specop_scan (EJSObject* obj, EJSValueFunc scan_func) { EJSRegExp *re = (EJSRegExp*)obj; - scan_func (re->pattern); - scan_func (re->flags); + scan_func (&(re->pattern)); + scan_func (&(re->flags)); _ejs_Object_specops.Scan (obj, scan_func); } diff --git a/runtime/ejs-runloop-libuv.c b/runtime/ejs-runloop-libuv.c index 8e01a792..7756b7be 100644 --- a/runtime/ejs-runloop-libuv.c +++ b/runtime/ejs-runloop-libuv.c @@ -11,8 +11,9 @@ typedef struct { EJSBool repeats; } task_timer; +// libuv >= 1.0 timer callbacks take only the handle static void -invoke_task(uv_timer_t* timer, int unused) +invoke_task(uv_timer_t* timer) { task_timer* t = (task_timer*)timer->data; t->task(t->data); diff --git a/runtime/ejs-set.c b/runtime/ejs-set.c index 7052b752..3944a3a5 100644 --- a/runtime/ejs-set.c +++ b/runtime/ejs-set.c @@ -230,6 +230,7 @@ _ejs_set_add(ejsval S, ejsval value) // 8. Append value as the last element of entries. e = calloc (1, sizeof (EJSSetValueEntry)); e->value = value; + _ejs_gc_remember(_set, e->value); if (!_set->head_insert) _set->head_insert = e; @@ -589,7 +590,7 @@ _ejs_set_specop_scan (EJSObject* obj, EJSValueFunc scan_func) EJSSet* set = (EJSSet*)obj; for (EJSSetValueEntry *s = set->head_insert; s; s = s->next_insert) - scan_func (s->value); + scan_func (&(s->value)); _ejs_Object_specops.Scan (obj, scan_func); } @@ -618,7 +619,7 @@ static void _ejs_set_iterator_specop_scan (EJSObject* obj, EJSValueFunc scan_func) { EJSSetIterator* iter = (EJSSetIterator*)obj; - scan_func(iter->iterated); + scan_func(&(iter->iterated)); _ejs_Object_specops.Scan (obj, scan_func); } diff --git a/runtime/ejs-shapes.c b/runtime/ejs-shapes.c new file mode 100644 index 00000000..83d0885b --- /dev/null +++ b/runtime/ejs-shapes.c @@ -0,0 +1,515 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=cpp: + * + * Runtime shape tracking. This module owns the + * global interned shape table and the transition cache; the + * object layer (ejs-object.c) stores shaped objects' property values in + * slot arrays at the indices this table dictates, via the transition / + * lookup API below. The census (dumped at exit under EJS_SHAPES_CENSUS) + * records what real programs do with it. + */ + +#include +#include + +#include "ejs.h" +#include "ejs-shapes.h" +#include "ejs-gc.h" +#include "ejs-ops.h" +#include "ejs-string.h" +#include "ejs-log.h" + +/* chunked, append-only shape storage: chunk addresses never move, so + &shape->name can be handed to _ejs_gc_add_root (the EJSShape record and + the chunk table itself live in ejs-shapes.h for the hot-path inlines) */ +#define SHAPE_MAX_SHAPES (1 << 24) /* the header gives us 24 bits */ +#define SHAPE_NUM_CHUNKS (SHAPE_MAX_SHAPES >> EJS_SHAPE_CHUNK_SHIFT) + +EJSShape *_ejs_shape_chunks[SHAPE_NUM_CHUNKS]; +static uint32_t shape_count; /* next unallocated index; starts at 2 (0 = + dictionary, 1 = root) */ + +EJSBool _ejs_shapes_tracking = EJS_FALSE; +static EJSBool census_enabled = EJS_FALSE; +/* runtime twin of maam's shapeCap; EJS_SHAPE_CAP overrides (clamped to + EJS_SHAPE_FIELD_CAP_MAX — see its comment in ejs-shapes.h for why the + ceiling is a page-allocator cell, not a semantic choice) */ +static uint32_t shape_field_cap = EJS_SHAPE_FIELD_CAP_MAX; + +/* transition cache: open-addressed (parent, name, repr) -> child. + child == 0 marks an empty slot (shape 0 is never a transition target) */ +typedef struct { + uint32_t parent; + uint32_t child; + uint32_t name_hash; /* rejects probe collisions without touching names */ +} TransitionEntry; + +static TransitionEntry *transitions; +static uint32_t transition_capacity; /* power of two */ +static uint32_t transition_count; + +/* census counters (the first three are bumped by the header inlines) */ +uint64_t _ejs_shape_stat_objects_born; +uint64_t _ejs_shape_stat_transitions; +uint64_t _ejs_shape_stat_cache_hits; +uint64_t _ejs_shape_stat_fast_hits; +static uint64_t stat_repr_flips; +static uint64_t stat_deaths_shaped; +static uint64_t stat_migrations[EJS_SHAPE_MIGRATE_NUM_REASONS]; +static uint32_t stat_max_depth; + +#define shape_get _ejs_shape_get + +/* returns the new shape's index, or EJS_SHAPE_DICT if the table is full. + stops one short of EJS_SHAPE_NOMATCH: that index must never be + allocatable, so a compiled guard against the sentinel is statically + false */ +static uint32_t +shape_alloc(uint32_t parent, ejsval name, uint8_t repr, uint32_t field_count) +{ + if (shape_count >= EJS_SHAPE_NOMATCH) + return EJS_SHAPE_DICT; + + uint32_t index = shape_count++; + uint32_t chunk = index >> EJS_SHAPE_CHUNK_SHIFT; + if (_ejs_shape_chunks[chunk] == NULL) + _ejs_shape_chunks[chunk] = (EJSShape *)calloc(EJS_SHAPE_CHUNK_SIZE, sizeof(EJSShape)); + + EJSShape *shape = shape_get(index); + shape->parent = parent; + shape->field_count = field_count; + shape->name = name; + shape->repr = repr; + /* the trace bitmap (gc-P5): parent's mask plus this edge's repr bit. + the root (field_count 0, parent DICT) gets 0. */ + shape->f64_mask = (field_count > 0 ? shape_get(parent)->f64_mask : 0) + | (repr == EJS_SHAPE_REPR_F64 && field_count > 0 + ? (1u << (field_count - 1)) : 0); + + /* keep the field name alive: shapes are process-global and never freed */ + if (EJSVAL_IS_STRING(name)) + _ejs_gc_add_root(&shape->name); + + return index; +} + +static uint32_t +transition_hash(uint32_t parent, uint32_t name_hash, uint8_t repr) +{ + uint32_t h = parent * 0x9e3779b9u; + h ^= name_hash + 0x9e3779b9u + (h << 6) + (h >> 2); + h ^= (uint32_t)repr + 0x9e3779b9u + (h << 6) + (h >> 2); + return h; +} + +static uint32_t +shape_name_hash(ejsval name) +{ + return _ejs_string_hash(name); +} + +/* property keys at a given site are almost always the same interned atom, + so raw ejsval equality catches nearly every cache hit; fall back to + content comparison for equal strings from different allocations */ +static EJSBool +shape_name_eq(ejsval a, ejsval b) +{ + if (EJSVAL_EQ(a, b)) + return EJS_TRUE; + return EJSVAL_TO_BOOLEAN(_ejs_op_strict_eq(a, b)); +} + +static void transition_insert(uint32_t parent, uint32_t name_hash, uint32_t child); + +static void +transition_grow(void) +{ + TransitionEntry *old = transitions; + uint32_t old_capacity = transition_capacity; + + transition_capacity = old_capacity ? old_capacity * 2 : 256; + transitions = (TransitionEntry *)calloc(transition_capacity, sizeof(TransitionEntry)); + transition_count = 0; + + for (uint32_t i = 0; i < old_capacity; i++) { + if (old[i].child == 0) + continue; + EJSShape *child_shape = shape_get(old[i].child); + transition_insert(old[i].parent, shape_name_hash(child_shape->name), + old[i].child); + } + free(old); +} + +static void +transition_insert(uint32_t parent, uint32_t name_hash, uint32_t child) +{ + if (transition_count + 1 > transition_capacity - (transition_capacity >> 2)) + transition_grow(); + + uint32_t mask = transition_capacity - 1; + uint32_t slot = transition_hash(parent, name_hash, shape_get(child)->repr) & mask; + while (transitions[slot].child != 0) + slot = (slot + 1) & mask; + transitions[slot].parent = parent; + transitions[slot].child = child; + transitions[slot].name_hash = name_hash; + transition_count++; +} + +/* the one hash hit per property add: find the (parent, name, repr) edge, + interning a new shape on first use. returns EJS_SHAPE_DICT only when + the global table is full. */ +static uint32_t +transition_find_or_add(uint32_t parent, ejsval name, uint8_t repr) +{ + EJSShape *parent_shape = shape_get(parent); + + /* the memo hit: same construction sequence as last time */ + uint32_t memo = parent_shape->last_child; + if (memo != EJS_SHAPE_DICT) { + EJSShape *m = shape_get(memo); + if (m->repr == repr && EJSVAL_EQ(m->name, name)) { + _ejs_shape_stat_cache_hits++; + return memo; + } + } + + if (transition_capacity == 0) + transition_grow(); + + uint32_t name_hash = shape_name_hash(name); + uint32_t mask = transition_capacity - 1; + uint32_t slot = transition_hash(parent, name_hash, repr) & mask; + + while (transitions[slot].child != 0) { + if (transitions[slot].parent == parent && + transitions[slot].name_hash == name_hash) { + EJSShape *cand = shape_get(transitions[slot].child); + if (cand->repr == repr && shape_name_eq(cand->name, name)) { + _ejs_shape_stat_cache_hits++; + parent_shape->last_child = transitions[slot].child; + return transitions[slot].child; + } + } + slot = (slot + 1) & mask; + } + + uint32_t child = shape_alloc(parent, name, repr, + parent_shape->field_count + 1); + if (child == EJS_SHAPE_DICT) + return EJS_SHAPE_DICT; + + transition_insert(parent, name_hash, child); + shape_get(parent)->last_child = child; /* shape_alloc may have grown chunks */ + return child; +} + +static uint8_t +classify_repr(ejsval value) +{ + return EJSVAL_IS_NUMBER(value) ? EJS_SHAPE_REPR_F64 : EJS_SHAPE_REPR_BOXED; +} + +void +_ejs_shape_object_migrate(EJSObject *obj, EJSShapeMigrateReason reason) +{ + if (EJS_OBJECT_SHAPE(obj) == EJS_SHAPE_DICT) + return; + EJS_OBJECT_SET_SHAPE(obj, EJS_SHAPE_DICT); + stat_migrations[reason]++; +} + +uint32_t +_ejs_shape_transition_add(uint32_t shape, ejsval name, ejsval value, + EJSShapeMigrateReason *reason) +{ + EJS_ASSERT(shape != EJS_SHAPE_DICT); + EJS_ASSERT(EJSVAL_IS_STRING(name)); + + /* numeric/index-looking keys stay in the map (arrays own indexed + storage; indexed access on plain objects is rare enough to eat it) */ + EJSPrimString *namestr = EJSVAL_TO_STRING(name); + if (namestr->length > 0) { + jschar c0 = EJS_PRIMSTR_GET_TYPE(namestr) == EJS_STRING_FLAT + ? namestr->data.flat[0] + : _ejs_string_ucs2_at(namestr, 0); + if (c0 >= '0' && c0 <= '9') { + *reason = EJS_SHAPE_MIGRATE_INDEX_KEY; + return EJS_SHAPE_DICT; + } + } + + EJSShape *cur = shape_get(shape); + if (cur->field_count >= shape_field_cap) { + *reason = EJS_SHAPE_MIGRATE_CAP; + return EJS_SHAPE_DICT; + } + + uint32_t child = transition_find_or_add(shape, name, classify_repr(value)); + if (child == EJS_SHAPE_DICT) { + *reason = EJS_SHAPE_MIGRATE_TABLE_FULL; + return EJS_SHAPE_DICT; + } + + _ejs_shape_stat_transitions++; + uint32_t depth = shape_get(child)->field_count; + if (depth > stat_max_depth) + stat_max_depth = depth; + return child; +} + +EJSBool +_ejs_shape_lookup(uint32_t shape, ejsval name, uint32_t *slot) +{ + uint32_t s = shape; + while (s != EJS_SHAPE_DICT) { + EJSShape *cur = shape_get(s); + if (cur->field_count == 0) + break; + if (shape_name_eq(cur->name, name)) { + *slot = cur->field_count - 1; + return EJS_TRUE; + } + s = cur->parent; + } + return EJS_FALSE; +} + +void +_ejs_shape_fields(uint32_t shape, ejsval *names) +{ + uint32_t s = shape; + for (uint32_t i = shape_get(shape)->field_count; i > 0; i--) { + EJSShape *cur = shape_get(s); + names[i - 1] = cur->name; + s = cur->parent; + } +} + +/* rebuild the chain with `field_index`'s repr changed: the sibling shape a + type-flipping store transitions to. returns EJS_SHAPE_DICT on table + overflow. */ +static uint32_t +shape_flip_repr(uint32_t shape, uint32_t field_index, uint8_t new_repr) +{ + /* collect edges leaf->root; depth is capped by shape_field_cap */ + ejsval names[256]; + uint8_t reprs[256]; + uint32_t depth = shape_get(shape)->field_count; + EJS_ASSERT(depth <= 256); + + uint32_t s = shape; + for (uint32_t i = depth; i > 0; i--) { + EJSShape *cur = shape_get(s); + names[i - 1] = cur->name; + reprs[i - 1] = cur->repr; + s = cur->parent; + } + reprs[field_index] = new_repr; + + uint32_t rebuilt = EJS_SHAPE_ROOT; + for (uint32_t i = 0; i < depth; i++) { + rebuilt = transition_find_or_add(rebuilt, names[i], reprs[i]); + if (rebuilt == EJS_SHAPE_DICT) + return EJS_SHAPE_DICT; + } + return rebuilt; +} + +uint32_t +_ejs_shape_transition_set(uint32_t shape, uint32_t slot_index, ejsval value) +{ + /* find the chain node owning this slot (its field_count is the + 1-based insertion index) */ + uint32_t s = shape; + EJSShape *cur = shape_get(s); + while (cur->field_count != slot_index + 1) { + s = cur->parent; + cur = shape_get(s); + } + + uint8_t new_repr = classify_repr(value); + if (new_repr == cur->repr) + return shape; + + uint32_t flipped = shape_flip_repr(shape, slot_index, new_repr); + if (flipped != EJS_SHAPE_DICT) + stat_repr_flips++; + return flipped; +} + +uint32_t +_ejs_shape_intern(uint32_t nfields, const ejsval *names, uint32_t f64_mask) +{ + if (!_ejs_shapes_tracking) + return EJS_SHAPE_NOMATCH; + /* the empty shape IS the root: fill_object_shaped's guard + (has_shape(this, "")) interns zero fields and must match the + construct-allocated empty receiver */ + if (nfields == 0) + return EJS_SHAPE_ROOT; + if (nfields > shape_field_cap || nfields > 32) + return EJS_SHAPE_NOMATCH; + + uint32_t shape = EJS_SHAPE_ROOT; + for (uint32_t i = 0; i < nfields; i++) { + ejsval name = names[i]; + if (!EJSVAL_IS_STRING(name)) + return EJS_SHAPE_NOMATCH; + + /* mirror _ejs_shape_transition_add's shapeability screen: an + index-looking key never enters the shaped world, so a shape + containing one can never match any object */ + EJSPrimString *namestr = EJSVAL_TO_STRING(name); + if (namestr->length > 0) { + jschar c0 = EJS_PRIMSTR_GET_TYPE(namestr) == EJS_STRING_FLAT + ? namestr->data.flat[0] + : _ejs_string_ucs2_at(namestr, 0); + if (c0 >= '0' && c0 <= '9') + return EJS_SHAPE_NOMATCH; + } + + /* a duplicate name would intern a corrupt chain (transition_find_ + or_add appends unconditionally; only the object layer's absent- + field discipline keeps runtime chains duplicate-free) */ + for (uint32_t j = 0; j < i; j++) + if (shape_name_eq(names[j], name)) + return EJS_SHAPE_NOMATCH; + + uint8_t repr = (f64_mask & (1u << i)) ? EJS_SHAPE_REPR_F64 + : EJS_SHAPE_REPR_BOXED; + shape = transition_find_or_add(shape, name, repr); + if (shape == EJS_SHAPE_DICT) + return EJS_SHAPE_NOMATCH; + } + return shape; +} + +void +_ejs_shape_object_died(EJSObject *obj) +{ + if (!census_enabled) + return; + uint32_t shape = EJS_OBJECT_SHAPE(obj); + if (shape == EJS_SHAPE_DICT) + return; + shape_get(shape)->deaths++; + stat_deaths_shaped++; +} + +/* ------------------------------------------------------------------ */ +/* census */ + +static void +census_print_shape_fields(uint32_t shape) +{ + ejsval names[256]; + uint8_t reprs[256]; + uint32_t depth = shape_get(shape)->field_count; + + uint32_t s = shape; + for (uint32_t i = depth; i > 0; i--) { + EJSShape *cur = shape_get(s); + names[i - 1] = cur->name; + reprs[i - 1] = cur->repr; + s = cur->parent; + } + + _ejs_logstr("{"); + for (uint32_t i = 0; i < depth; i++) { + if (i > 0) + _ejs_logstr(", "); + char *utf8 = ucs2_to_utf8(EJSVAL_TO_FLAT_STRING(names[i])); + _ejs_logstr(utf8); + free(utf8); + if (reprs[i] == EJS_SHAPE_REPR_F64) + _ejs_logstr(":f64"); + } + _ejs_logstr("}"); +} + +static void +census_dump(void) +{ + static const char *reason_names[EJS_SHAPE_MIGRATE_NUM_REASONS] = { + "delete", "attrs", "accessor", "symbol-key", + "index-key", "cap", "table-full", + }; + + uint64_t migrations_total = 0; + for (int i = 0; i < EJS_SHAPE_MIGRATE_NUM_REASONS; i++) + migrations_total += stat_migrations[i]; + + _ejs_log("=== ejs shape census ===\n"); + _ejs_log("objects born tracked: %llu\n", + (unsigned long long)_ejs_shape_stat_objects_born); + _ejs_log("shapes interned: %u (max depth %u)\n", + shape_count > 2 ? shape_count - 2 : 0, stat_max_depth); + _ejs_log("transitions: %llu (fast hits %llu, cache hits %llu)\n", + (unsigned long long)_ejs_shape_stat_transitions, + (unsigned long long)_ejs_shape_stat_fast_hits, + (unsigned long long)_ejs_shape_stat_cache_hits); + _ejs_log("repr flips: %llu\n", + (unsigned long long)stat_repr_flips); + _ejs_log("dictionary migrations: %llu\n", + (unsigned long long)migrations_total); + for (int i = 0; i < EJS_SHAPE_MIGRATE_NUM_REASONS; i++) + if (stat_migrations[i]) + _ejs_log(" %-11s %llu\n", reason_names[i], + (unsigned long long)stat_migrations[i]); + _ejs_log("deaths while shaped: %llu\n", + (unsigned long long)stat_deaths_shaped); + + /* top shapes by death count */ + uint32_t top[16]; + uint32_t ntop = 0; + for (uint32_t i = 2; i < shape_count; i++) { + if (shape_get(i)->deaths == 0) + continue; + uint32_t pos = ntop < 16 ? ntop : 15; + if (ntop == 16 && shape_get(i)->deaths <= shape_get(top[15])->deaths) + continue; + while (pos > 0 && shape_get(top[pos - 1])->deaths < shape_get(i)->deaths) { + top[pos] = top[pos - 1]; + pos--; + } + top[pos] = i; + if (ntop < 16) + ntop++; + } + if (ntop > 0) { + _ejs_log("top shapes at death:\n"); + for (uint32_t i = 0; i < ntop; i++) { + _ejs_log(" %8u ", shape_get(top[i])->deaths); + census_print_shape_fields(top[i]); + _ejs_logstr("\n"); + } + } +} + +void +_ejs_shapes_init(void) +{ + const char *shapes_env = getenv("EJS_SHAPES"); + _ejs_shapes_tracking = + !(shapes_env && (!strcmp(shapes_env, "off") || !strcmp(shapes_env, "0"))); + if (!_ejs_shapes_tracking) + return; + + const char *cap_env = getenv("EJS_SHAPE_CAP"); + if (cap_env) { + int cap = atoi(cap_env); + if (cap > 0 && cap <= EJS_SHAPE_FIELD_CAP_MAX) + shape_field_cap = (uint32_t)cap; + } + + /* index 0 is dictionary mode; index 1 is the empty root shape */ + shape_count = 1; + shape_alloc(EJS_SHAPE_DICT, _ejs_undefined, EJS_SHAPE_REPR_BOXED, 0); + + if (getenv("EJS_SHAPES_CENSUS")) { + census_enabled = EJS_TRUE; + atexit(census_dump); + } +} diff --git a/runtime/ejs-shapes.h b/runtime/ejs-shapes.h new file mode 100644 index 00000000..c7ad9e60 --- /dev/null +++ b/runtime/ejs-shapes.h @@ -0,0 +1,220 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: set ts=4 sw=4 et tw=99 ft=cpp: + * + * Runtime shape tracking. + * + * A shape is a transition edge (parent, name, repr) appended to a parent + * shape; the global table is interned and append-only, mirroring maam's + * type-aware hidden classes one-for-one (repr is part of shape identity). + * The shape IS the property structure for shaped-mode ordinary + * objects: their values live in a slot array at shape-determined indices + * (the storage engine is in ejs-object.c; this module owns the shape + * table and answers name->slot / transition queries). Anything the + * shaped world can't express (deletes, non-default attributes, accessors, + * symbol/index keys, cap overflow) drops the object to dictionary mode + * (shape index 0) one-way — the map path — with the reason counted for + * the census. + * + * EJS_SHAPES=off disables tracking entirely; EJS_SHAPES_CENSUS=1 dumps + * the shape census at exit. + */ + +#ifndef _ejs_shapes_h_ +#define _ejs_shapes_h_ + +#include "ejs.h" +#include "ejs-object.h" + +EJS_BEGIN_DECLS + +/* field representation, part of shape identity (mirrors maam's TypeSig + abstraction; finer tags later if the census says they pay) */ +typedef enum { + EJS_SHAPE_REPR_BOXED = 0, + EJS_SHAPE_REPR_F64 = 1, +} EJSShapeRepr; + +/* shape index 0 = dictionary mode (or a class that never tracks); + shape index 1 = the empty ordinary-object shape */ +#define EJS_SHAPE_DICT 0 +#define EJS_SHAPE_ROOT 1 + +/* the never-matches sentinel compiled shape guards compare against when a + module's shape could not be interned (EJS_SHAPES=off, index-looking key, + cap, table full). The table never allocates this index (shape_alloc + stops one short), so no object header can ever carry it — a guard + against it is statically false, and the guarded slow path serves every + access. */ +#define EJS_SHAPE_NOMATCH 0xFFFFFFu + +/* hard ceiling on shaped field count (and EJS_SHAPE_CAP): a full + OUT-OF-LINE slot array must fit a page cell without waste — 16-byte + EJSClosureEnv header + 14 * 8-byte slots = 128 exactly — and the + single-cell embedded form fits the 256-byte class (32+16+112 = 160; + the class was LOS-routed by an ffs off-by-one until gc-P5 enabled + it on top of gc-P4's LOS bsearch + direct arena map). Objects with + more fields drop to dictionary mode — the original map world. */ +#define EJS_SHAPE_FIELD_CAP_MAX 14 + +/* single-cell (embedded-slots) allocation cap: object header (32) + + embedded env header (16) + 8 * fields. With the 256-byte size class + enabled (gc-P5), every cap-14 shape fits a page cell (32+16+112 = + 160 -> 256), so the embed cap IS the field cap. */ +#define EJS_SHAPE_EMBED_FIELD_MAX EJS_SHAPE_FIELD_CAP_MAX + +/* the shape index lives in bits 32-55 of the 64-bit GCObjectHeader (bit + 56 is the storage-mode bit; 57-63 belong to the GC) — see the + layout comment in ejs-types.h */ +#define EJS_GC_HEADER_SHAPE_SHIFT 32 +#define EJS_GC_HEADER_SHAPE_MASK 0xFFFFFFULL + +#define EJS_OBJECT_SHAPE(o) \ + ((uint32_t)((((EJSObject *)(o))->gc_header >> EJS_GC_HEADER_SHAPE_SHIFT) & \ + EJS_GC_HEADER_SHAPE_MASK)) +#define EJS_OBJECT_SET_SHAPE(o, s) \ + (((EJSObject *)(o))->gc_header = \ + (((EJSObject *)(o))->gc_header & \ + ~(EJS_GC_HEADER_SHAPE_MASK << EJS_GC_HEADER_SHAPE_SHIFT)) | \ + (((uint64_t)(s) & EJS_GC_HEADER_SHAPE_MASK) \ + << EJS_GC_HEADER_SHAPE_SHIFT)) + +/* dictionary-migration reasons, census-counted */ +typedef enum { + EJS_SHAPE_MIGRATE_DELETE = 0, /* delete of a tracked field */ + EJS_SHAPE_MIGRATE_ATTRS, /* non-default w/e/c attribute (incl. freeze/seal) */ + EJS_SHAPE_MIGRATE_ACCESSOR, /* getter/setter definition or conversion */ + EJS_SHAPE_MIGRATE_SYMBOL_KEY, /* symbol-keyed property */ + EJS_SHAPE_MIGRATE_INDEX_KEY, /* numeric/index-looking key */ + EJS_SHAPE_MIGRATE_CAP, /* per-object field-count cap overflow */ + EJS_SHAPE_MIGRATE_TABLE_FULL, /* global shape-table pathology */ + EJS_SHAPE_MIGRATE_NUM_REASONS +} EJSShapeMigrateReason; + +void _ejs_shapes_init(void); + +/* the shape record and chunk table are exposed only so the hot-path + inlines below can avoid a cross-TU call per property insert; everything + else treats them as private to ejs-shapes.c */ +typedef struct { + uint32_t parent; /* parent shape index (EJS_SHAPE_DICT for the root) */ + uint32_t field_count; /* own fields including this edge (root = 0) */ + ejsval name; /* this edge's field name; gc-rooted (chunks are + address-stable) */ + uint8_t repr; /* EJSShapeRepr, part of shape identity */ + uint32_t last_child; /* memo of the most recent transition taken from + this shape; monomorphic construction sites hit + it every time and skip the hash entirely */ + uint32_t deaths; /* census: objects finalized bearing this shape */ + uint32_t f64_mask; /* the shape's trace bitmap (gc-P5): bit i set = + field i is EJS_SHAPE_REPR_F64, i.e. a raw + double the collector can skip. Built + incrementally (parent's mask | this edge) so + every walk is O(1); field cap 14 << 32 bits */ +} EJSShape; + +#define EJS_SHAPE_CHUNK_SHIFT 12 +#define EJS_SHAPE_CHUNK_SIZE (1 << EJS_SHAPE_CHUNK_SHIFT) + +extern EJSShape *_ejs_shape_chunks[]; +extern EJSBool _ejs_shapes_tracking; +extern uint64_t _ejs_shape_stat_objects_born; +extern uint64_t _ejs_shape_stat_transitions; +extern uint64_t _ejs_shape_stat_cache_hits; +extern uint64_t _ejs_shape_stat_fast_hits; + +static inline EJSShape * +_ejs_shape_get(uint32_t index) +{ + return &_ejs_shape_chunks[index >> EJS_SHAPE_CHUNK_SHIFT] + [index & (EJS_SHAPE_CHUNK_SIZE - 1)]; +} + +/* object-side hooks */ + +/* an ordinary object was just initialized: give it the root shape */ +static inline void +_ejs_shape_object_born(EJSObject *obj) +{ + if (!_ejs_shapes_tracking) + return; + EJS_OBJECT_SET_SHAPE(obj, EJS_SHAPE_ROOT); + _ejs_shape_stat_objects_born++; +} + +/* something un-shapeable happened: one-way drop to dictionary mode. + Only flips the header index and counts the reason — materializing the + map from slot storage is the object layer's job + (_ejs_object_to_dictionary in ejs-object.c) */ +void _ejs_shape_object_migrate(EJSObject *obj, EJSShapeMigrateReason reason); + +/* finalizer hook, census only */ +void _ejs_shape_object_died(EJSObject *obj); + +/* shape-table queries for the object layer's storage engine. + None of these touch any object. */ + +/* number of own fields of `shape` */ +static inline uint32_t +_ejs_shape_field_count(uint32_t shape) +{ + return _ejs_shape_get(shape)->field_count; +} + +/* find string key `name` among shape's fields; on hit returns EJS_TRUE + with *slot = the field's insertion-ordered index */ +EJSBool _ejs_shape_lookup(uint32_t shape, ejsval name, uint32_t *slot); + +/* fill names[0 .. field_count) with the field names in insertion + (root->leaf) order; names must have room for field_count entries */ +void _ejs_shape_fields(uint32_t shape, ejsval *names); + +/* transition for inserting a new own data property `name` (a string, + caller-checked) with default attributes and initial value `value`. + Returns the child shape index, or EJS_SHAPE_DICT with *reason set when + the add can't stay shaped (index-looking key, field cap, table full). */ +uint32_t _ejs_shape_transition_add(uint32_t shape, ejsval name, ejsval value, + EJSShapeMigrateReason *reason); + +/* inline fast path for property adds: when the parent shape's transition + memo matches (same name ejsval, same repr — the monomorphic + construction sequence), the name was already vetted as a shapeable key + when the memo's shape was interned and its field count already passed + the cap, so every check collapses into one compare */ +static inline uint32_t +_ejs_shape_transition_add_fast(uint32_t shape, ejsval name, ejsval value, + EJSShapeMigrateReason *reason) +{ + uint32_t memo = _ejs_shape_get(shape)->last_child; + if (memo != EJS_SHAPE_DICT) { + EJSShape *m = _ejs_shape_get(memo); + uint8_t repr = EJSVAL_IS_NUMBER(value) ? EJS_SHAPE_REPR_F64 + : EJS_SHAPE_REPR_BOXED; + if (EJSVAL_EQ(m->name, name) && m->repr == repr) { + _ejs_shape_stat_transitions++; + _ejs_shape_stat_fast_hits++; + return memo; + } + } + return _ejs_shape_transition_add(shape, name, value, reason); +} + +/* transition for storing `value` into the existing field at + `slot_index`: returns `shape` when the repr is unchanged, the + repr-flipped sibling shape otherwise, or EJS_SHAPE_DICT on shape-table + overflow */ +uint32_t _ejs_shape_transition_set(uint32_t shape, uint32_t slot_index, + ejsval value); + +/* module-init interning for compiled shape guards (the + atom-table precedent): walk/intern the ordered shape whose fields are + names[0..nfields) with reprs from f64_mask (bit i set = field i is + EJS_SHAPE_REPR_F64), returning its index for the module's shape global. + Returns EJS_SHAPE_NOMATCH when the shape can't exist (tracking off, + index-looking key, over the field cap, table full) — guards against + NOMATCH are simply always false. */ +uint32_t _ejs_shape_intern(uint32_t nfields, const ejsval *names, + uint32_t f64_mask); + +EJS_END_DECLS + +#endif /* _ejs_shapes_h_ */ diff --git a/runtime/ejs-string.c b/runtime/ejs-string.c index 4fe2a3f9..99e4f392 100644 --- a/runtime/ejs-string.c +++ b/runtime/ejs-string.c @@ -1941,7 +1941,7 @@ static void _ejs_string_specop_scan (EJSObject* obj, EJSValueFunc scan_func) { EJSString* ejss = (EJSString*)obj; - scan_func (ejss->primStr); + scan_func (&(ejss->primStr)); _ejs_Object_specops.Scan (obj, scan_func); } @@ -1971,7 +1971,7 @@ static void _ejs_string_iterator_specop_scan (EJSObject* obj, EJSValueFunc scan_func) { EJSStringIterator* iter = (EJSStringIterator*)obj; - scan_func(iter->iterated); + scan_func(&(iter->iterated)); _ejs_Object_specops.Scan (obj, scan_func); } @@ -2211,6 +2211,10 @@ static void flatten_dep (jschar **p, EJSPrimString *n, int* off, int* len); static void flatten_rope (jschar **p, EJSPrimString *n) { + if ((n->gc_header & 0xffffffff) == 0xafafafaf) { + _ejs_log ("flatten_rope: POISONED node %p\n", (void*)n); + abort(); + } switch (EJS_PRIMSTR_GET_TYPE(n)) { case EJS_STRING_FLAT: memmove (*p, n->data.flat, n->length * sizeof(jschar)); diff --git a/runtime/ejs-symbol.c b/runtime/ejs-symbol.c index 4fe326b7..f6e67cc9 100644 --- a/runtime/ejs-symbol.c +++ b/runtime/ejs-symbol.c @@ -11,12 +11,14 @@ // ECMA262: 19.4.2.2 Symbol.for ( key ) static EJS_NATIVE_FUNC(_ejs_Symbol_for) { +#if notyet ejsval key = _ejs_undefined; if (argc > 0) key = args[0]; // 1. Let stringKey be ToString(key). // 2. ReturnIfAbrupt(stringKey). ejsval stringKey = ToString(key); +#endif // 3. For each element e of the GlobalSymbolRegistry List, // a. If SameValue(e.[[key]], stringKey) is true, then return e.[[symbol]]. @@ -219,7 +221,7 @@ _ejs_symbol_specop_allocate () static void _ejs_symbol_specop_scan (EJSObject* obj, EJSValueFunc scan_func) { - scan_func(((EJSSymbol*)obj)->primSymbol); + scan_func(&(((EJSSymbol*)obj)->primSymbol)); } EJS_DEFINE_CLASS(Symbol, diff --git a/runtime/ejs-typedarrays.c b/runtime/ejs-typedarrays.c index d348cc70..f32c03c6 100644 --- a/runtime/ejs-typedarrays.c +++ b/runtime/ejs-typedarrays.c @@ -413,18 +413,23 @@ EJS_DATA_VIEW_METHOD_IMPL(Float64, double, 8); /* TypedArray(ArrayBuffer buffer, unsigned long byteOffset, unsigned long length) */ \ uint32_t byteOffset = 0; \ uint32_t byteLength = buffer->size; \ + uint32_t requestedLength = 0; \ EJSBool lengthSpecified = EJS_FALSE; \ \ if (argc > 1) byteOffset = ToUint32(args[1]); \ if (argc > 2) { \ - byteLength = ToUint32(args[2]) * elementSizeInBytes; \ + requestedLength = ToUint32(args[2]); \ + byteLength = requestedLength * elementSizeInBytes; \ lengthSpecified = EJS_TRUE; \ } \ \ if (byteOffset > buffer->size) byteOffset = buffer->size; \ if (byteOffset + byteLength > buffer->size) { \ - if (lengthSpecified) \ - _ejs_throw_nativeerror_utf8 (EJS_RANGE_ERROR, "Length is out of range."); \ + if (lengthSpecified) { \ + char rangemsg[64]; \ + snprintf (rangemsg, sizeof(rangemsg), "Invalid typed array length: %u", requestedLength); \ + _ejs_throw_nativeerror_utf8 (EJS_RANGE_ERROR, rangemsg); \ + } \ else \ byteLength = buffer->size - byteOffset; \ } \ @@ -2487,7 +2492,7 @@ _ejs_arraybuffer_specop_scan (EJSObject* obj, EJSValueFunc scan_func) { EJSArrayBuffer *arraybuf = (EJSArrayBuffer*)obj; if (arraybuf->dependent) { - scan_func (arraybuf->data.dependent.buf); + scan_func (&(arraybuf->data.dependent.buf)); } _ejs_Object_specops.Scan (obj, scan_func); } @@ -2529,128 +2534,10 @@ static void _ejs_typedarray_specop_scan (EJSObject* obj, EJSValueFunc scan_func) { EJSTypedArray *arr = (EJSTypedArray*)obj; - scan_func(arr->buffer); + scan_func(&(arr->buffer)); _ejs_Object_specops.Scan (obj, scan_func); } -static ejsval -_ejs_dataview_specop_get (ejsval obj, ejsval propertyName, ejsval receiver) -{ - // check if propertyName is an integer, or a string that we can convert to an int - EJSBool is_index = EJS_FALSE; - int idx = 0; - if (EJSVAL_IS_NUMBER(propertyName)) { - double n = EJSVAL_TO_NUMBER(propertyName); - if (floor(n) == n) { - idx = (int)n; - is_index = EJS_TRUE; - } - } - - // Index for DataView is byte-based. - if (is_index) { - if (idx < 0 || idx > EJS_DATA_VIEW_BYTE_LEN(obj)) - return _ejs_undefined; - - void *data = _ejs_dataview_get_data (EJSVAL_TO_OBJECT(obj)); - return NUMBER_TO_EJSVAL ((double)((unsigned char*)data)[idx]); - } - - // otherwise we fallback to the object implementation - return _ejs_Object_specops.Get (obj, propertyName, receiver); -} - -static EJSPropertyDesc* -_ejs_dataview_specop_get_own_property (ejsval obj, ejsval propertyName, ejsval* exc) -{ - if (EJSVAL_IS_NUMBER(propertyName)) { - double needle = EJSVAL_TO_NUMBER(propertyName); - int needle_int; - if (EJSDOUBLE_IS_INT32(needle, &needle_int)) { - if (needle_int >= 0 && needle_int < EJS_DATA_VIEW_BYTE_LEN(obj)) - return NULL; // XXX - } - } - - return _ejs_Object_specops.GetOwnProperty (obj, propertyName, exc); -} - -static EJSBool -_ejs_dataview_specop_set (ejsval obj, ejsval propertyName, ejsval val, ejsval receiver) -{ - EJSBool is_index = EJS_FALSE; - ejsval idx_val; - int idx; - - if (!EJSVAL_IS_SYMBOL(propertyName)) { - idx_val = ToNumber(propertyName); - if (EJSVAL_IS_NUMBER(idx_val)) { - double n = EJSVAL_TO_NUMBER(idx_val); - if (floor(n) == n) { - idx = (int)n; - is_index = EJS_TRUE; - } - } - } - - if (is_index) { - if (idx < 0 || idx >= EJS_DATA_VIEW_BYTE_LEN(obj)) - return EJS_FALSE; - - void* data = _ejs_dataview_get_data (EJSVAL_TO_OBJECT(obj)); - ((unsigned char*)data)[idx] = (unsigned char)EJSVAL_TO_NUMBER(val); - - return EJS_TRUE; - } - - return _ejs_Object_specops.Set (obj, propertyName, val, receiver); -} - -static EJSBool -_ejs_dataview_specop_has_property (ejsval obj, ejsval propertyName) -{ - // check if propertyName is a uint32, or a string that we can convert to an uint32 - int idx = -1; - if (EJSVAL_IS_NUMBER(propertyName)) { - double n = EJSVAL_TO_NUMBER(propertyName); - if (floor(n) == n) { - idx = (int)n; - - return idx > 0 && idx < EJS_DATA_VIEW_BYTE_LEN(obj); - } - } - - return _ejs_Object_specops.HasProperty (obj, propertyName); -} - -static EJSBool -_ejs_dataview_specop_delete (ejsval obj, ejsval propertyName, EJSBool flag) -{ - int idx = -1; - if (EJSVAL_IS_NUMBER(propertyName)) { - double n = EJSVAL_TO_NUMBER(propertyName); - if (floor(n) == n) { - idx = (int)n; - } - } - - if (idx == -1) - return _ejs_Object_specops.Delete (obj, propertyName, flag); - - if (idx < EJS_DATA_VIEW_BYTE_LEN(obj)) { - //void* data = _ejs_dataview_get_data (EJSVAL_TO_OBJECT(obj)); - //((unsigned char*)data)[idx] = _ejs_undefined; - } - - return EJS_FALSE; -} - -static EJSBool -_ejs_dataview_specop_define_own_property (ejsval obj, ejsval propertyName, EJSPropertyDesc* propertyDescriptor, EJSBool flag) -{ - return _ejs_Object_specops.DefineOwnProperty (obj, propertyName, propertyDescriptor, flag); -} - static EJSObject* _ejs_dataview_specop_allocate () { @@ -2661,21 +2548,26 @@ static void _ejs_dataview_specop_scan (EJSObject* obj, EJSValueFunc scan_func) { EJSDataView *view = (EJSDataView*)obj; - scan_func (view->buffer); + scan_func (&(view->buffer)); _ejs_Object_specops.Scan (obj, scan_func); } +// DataView is NOT an integer-indexed exotic object (unlike the +// TypedArrays): view[i] is an ordinary property, byte access goes +// through get/setUint8 etc. ejs used to route indexes at the +// underlying buffer here, which typedarray5 caught once the harness +// went value-based (runtime-P3). EJS_DEFINE_CLASS(DataView, OP_INHERIT, // [[GetPrototypeOf]] OP_INHERIT, // [[SetPrototypeOf]] OP_INHERIT, // [[IsExtensible]] OP_INHERIT, // [[PreventExtensions]] - _ejs_dataview_specop_get_own_property, - _ejs_dataview_specop_define_own_property, - _ejs_dataview_specop_has_property, - _ejs_dataview_specop_get, - _ejs_dataview_specop_set, - _ejs_dataview_specop_delete, + OP_INHERIT, // [[GetOwnProperty]] + OP_INHERIT, // [[DefineOwnProperty]] + OP_INHERIT, // [[HasProperty]] + OP_INHERIT, // [[Get]] + OP_INHERIT, // [[Set]] + OP_INHERIT, // [[Delete]] OP_INHERIT, // [[Enumerate]] OP_INHERIT, // [[OwnPropertyKeys]] OP_INHERIT, // [[Call]] diff --git a/runtime/ejs-types.h b/runtime/ejs-types.h index b1d9bec4..a3cde297 100644 --- a/runtime/ejs-types.h +++ b/runtime/ejs-types.h @@ -27,7 +27,29 @@ typedef double jsdouble; typedef uint16_t jschar; -typedef uint32_t GCObjectHeader; +// The object header, widened to 64 bits as one joint GC/shapes layout +// (written once — see docs/gc-plan.md "Object header, forwarding, and +// shapes" and docs/shapes-plan.md "Object layout, in two steps"): +// +// bits 0-31 the pre-existing 32-bit header: EJSScanType in the low +// bits, user flags at EJS_GC_USER_FLAGS_SHIFT (unchanged) +// bits 32-55 shape index (0 = dictionary mode / untracked) +// bit 56 shaped-storage mode bit +// bit 57 YOUNG — allocated since the last collection (profiling +// profiling; a nursery age bit in waiting) +// bit 58 PINNED — conservatively referenced this cycle (profiling +// profiling, cleared each cycle) +// bit 59 FORWARDED — the word is a forwarding record, not a +// header: target address in bits 0-46 (see +// ejs-gc.h _ejs_gc_forward) +// bit 60 DIRTY — the object is in the generational remembered +// buffer (object-remembering write barrier) +// bits 61-63 reserved for the GC (future mark/card bits) +// +// EJSObject absorbs the widening into what was padding (sizeof +// unchanged); EJSPrimString/EJSPrimSymbol keep their sizes; EJSClosureEnv +// grows by 8. lib/types.ts mirrors this in the same commit. +typedef uint64_t GCObjectHeader; #if defined(__GNUC__) && (__GNUC__ > 2) # define EJS_LIKELY(x) (__builtin_expect((x), 1)) diff --git a/runtime/ejs-value.h b/runtime/ejs-value.h index 81a2050c..42af4dad 100644 --- a/runtime/ejs-value.h +++ b/runtime/ejs-value.h @@ -59,7 +59,10 @@ ejsval _ejs_number_new (double value); void _ejs_value_finalize(ejsval val); -typedef void (*EJSValueFunc)(ejsval value); +// scan callbacks take the SLOT, not the value — the mover +// rewrites *slot when the referent is evacuated. Non-moving consumers +// (the old mark path) simply read through it. +typedef void (*EJSValueFunc)(ejsval* slot); EJS_END_DECLS diff --git a/runtime/gen-atoms.js b/runtime/gen-atoms.js deleted file mode 100755 index 11a08a4c..00000000 --- a/runtime/gen-atoms.js +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env node -const fs = require("fs"); - -let atom_def = fs.readFileSync(process.argv[2], "utf-8"); - -let atom_lines = atom_def.split("\n"); -let new_lines = []; -let atom_names = []; - -for (const atom_line of atom_lines) { - let atom = null; - let atom_name = null; - - let match = atom_line.match(/^EJS_ATOM\((.*)\)$/); - let match2 = atom_line.match(/^EJS_ATOM2\((.*),(.*)\)$/); - let match3 = atom_line.match(/^EJS_ATOM2\(,(.*)\)$/); - - if (match) { - atom = match[1]; - atom_name = match[1]; - } else if (match2) { - atom = match2[1]; - atom_name = match2[2]; - } else if (match3) { - atom = ""; - atom_name = match3[1]; - } - - if (atom === null) { - new_lines.push(atom_line); - continue; - } - - // output the ucs2 literal for the atom - let line = `const jschar _ejs_ucs2_${atom_name}[] EJSVAL_ALIGNMENT = { `; - for (let cn = 0, ce = atom.length; cn < ce; cn++) { - const code = atom.charCodeAt(cn); - const hex = new Number(code).toString(16); - if (code < 0x10) line += `0x000${hex}`; - else if (code < 0x100) line += `0x00${hex}`; - else if (code < 0x1000) line += `0x0${hex}`; - else line += `0x${hex}`; - - line += ", "; - } - line += "0x0000 };"; - new_lines.push(line); - - new_lines.push( - `static EJSPrimString _ejs_primstring_${atom_name} EJSVAL_ALIGNMENT = { .gc_header = (EJS_STRING_FLAT<"); + process.exit(1); +} + +const atom_def = fs.readFileSync(input, "utf-8"); + +const atom_lines = atom_def.split("\n"); +const new_lines: string[] = []; +const atom_names: string[] = []; + +for (const atom_line of atom_lines) { + let atom: string | null = null; + let atom_name: string | null = null; + + const match = atom_line.match(/^EJS_ATOM\((.*)\)$/); + const match2 = atom_line.match(/^EJS_ATOM2\((.*),(.*)\)$/); + const match3 = atom_line.match(/^EJS_ATOM2\(,(.*)\)$/); + + if (match) { + atom = match[1] ?? ""; + atom_name = match[1] ?? ""; + } else if (match2) { + atom = match2[1] ?? ""; + atom_name = match2[2] ?? ""; + } else if (match3) { + atom = ""; + atom_name = match3[1] ?? ""; + } + + if (atom === null || atom_name === null) { + new_lines.push(atom_line); + continue; + } + + // output the ucs2 literal for the atom + let line = `const jschar _ejs_ucs2_${atom_name}[] EJSVAL_ALIGNMENT = { `; + for (let cn = 0, ce = atom.length; cn < ce; cn++) { + const code = atom.charCodeAt(cn); + const hex = code.toString(16); + if (code < 0x10) line += `0x000${hex}`; + else if (code < 0x100) line += `0x00${hex}`; + else if (code < 0x1000) line += `0x0${hex}`; + else line += `0x${hex}`; + + line += ", "; + } + line += "0x0000 };"; + new_lines.push(line); + + new_lines.push( + `static EJSPrimString _ejs_primstring_${atom_name} EJSVAL_ALIGNMENT = { .gc_header = (EJS_STRING_FLAT< { return x * x; diff --git a/test/arrow2.js b/test/arrow2.js index 22bd0485..3ebae905 100644 --- a/test/arrow2.js +++ b/test/arrow2.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm function Multiplier(factor) { this.factor = factor; diff --git a/test/arrow3.js b/test/arrow3.js index c3eefbaa..c378320b 100644 --- a/test/arrow3.js +++ b/test/arrow3.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm // lexical "arguments" binding from kangax var f = (function () { diff --git a/test/class1.js b/test/class1.js index 0c268b03..f9c94c72 100644 --- a/test/class1.js +++ b/test/class1.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm class TestClass { constructor() { diff --git a/test/class2.js b/test/class2.js index 1e4ea72e..8283c1ea 100644 --- a/test/class2.js +++ b/test/class2.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm class SuperClass { constructor() { diff --git a/test/class3.js b/test/class3.js index 17577a72..9d090d87 100644 --- a/test/class3.js +++ b/test/class3.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm class Supest { constructor(foo) { diff --git a/test/class4.js b/test/class4.js index d11fb01b..ed44b2a2 100644 --- a/test/class4.js +++ b/test/class4.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm // methods aren't enumerable from kangax class C { foo() {} diff --git a/test/class5.js b/test/class5.js index e1a5e083..f6d7ba61 100644 --- a/test/class5.js +++ b/test/class5.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm // implicit strict mode from kangax class C { diff --git a/test/class6.js b/test/class6.js index 0a091ec2..7d56daa9 100644 --- a/test/class6.js +++ b/test/class6.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm class C { foo() { console.log("hi"); diff --git a/test/closure3.js b/test/closure3.js index a5f2a53a..6b5cdd0a 100644 --- a/test/closure3.js +++ b/test/closure3.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm let a = 5; function b() { diff --git a/test/closure7.js b/test/closure7.js index 769f1fa2..6ecd030b 100644 --- a/test/closure7.js +++ b/test/closure7.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm function a() { let x = 5; diff --git a/test/codepoint-eq1.js b/test/codepoint-eq1.js index 5e4d8671..63f8b664 100644 --- a/test/codepoint-eq1.js +++ b/test/codepoint-eq1.js @@ -1,2 +1,2 @@ -// generator: babel-node +// generator: esm console.log("\u{1d306}" == "\ud834\udf06"); diff --git a/test/computed-props1.js b/test/computed-props1.js index 973d4556..02786b94 100644 --- a/test/computed-props1.js +++ b/test/computed-props1.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm var x = "y"; console.log({ [x]: 1 }["y"] === 1); diff --git a/test/computed-props3.js b/test/computed-props3.js index fe34c633..02cf03c1 100644 --- a/test/computed-props3.js +++ b/test/computed-props3.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm var x = "y"; function foo() { diff --git a/test/const1.js b/test/const1.js index bb1b1062..c5495ca2 100644 --- a/test/const1.js +++ b/test/const1.js @@ -1,4 +1,4 @@ -// we disable generation here because babel-node errors out when we reassign i +// we disable generation here because node errors out when we reassign i // below. // generator: none // xfail: we permit assigning to const bindings diff --git a/test/date3.js b/test/date3.js index 3d4e07be..44deeb98 100644 --- a/test/date3.js +++ b/test/date3.js @@ -1,4 +1,2 @@ -// xfail: the first date is off by an hour. timegm/localtime_r screwup? - console.log(new Date(2000, 8)); console.log(new Date(2000, 0)); diff --git a/test/defaultargs1.js b/test/defaultargs1.js index c43da8be..873717fb 100644 --- a/test/defaultargs1.js +++ b/test/defaultargs1.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm function foo(a = "hello") { console.log(a); diff --git a/test/defaultargs2.js b/test/defaultargs2.js index b018ec2a..0f69f78b 100644 --- a/test/defaultargs2.js +++ b/test/defaultargs2.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm function foo(b, a = "hello") { console.log(a); diff --git a/test/destructure1.js b/test/destructure1.js index 67ff837e..2c8af955 100644 --- a/test/destructure1.js +++ b/test/destructure1.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm function foo({ x, y }) { console.log(x + " + " + y); diff --git a/test/destructure2.js b/test/destructure2.js index 67fa564a..42eb2d4d 100644 --- a/test/destructure2.js +++ b/test/destructure2.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm let foo = [1, 2]; let [a, b] = foo; diff --git a/test/destructure3.js b/test/destructure3.js index 0ebb775a..d1dd0c62 100644 --- a/test/destructure3.js +++ b/test/destructure3.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm let { x, y } = { x: "hello", y: "world" }; diff --git a/test/destructure4.js b/test/destructure4.js index e9c3aea4..ab81bb40 100644 --- a/test/destructure4.js +++ b/test/destructure4.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm let a, b, c, d; [a, b] = ["hello", "world"]; diff --git a/test/eir-accessor1.js b/test/eir-accessor1.js new file mode 100644 index 00000000..68f2c109 --- /dev/null +++ b/test/eir-accessor1.js @@ -0,0 +1,46 @@ +// object-literal accessors through EIR (define_accessor); a get/set +// PAIR for one property must keep both halves + +function pair() { + let backing = 5; + let o = { + tag: "t", + get n() { return backing * 10; }, + set n(v) { backing = v + 1; }, + }; + let before = o.n; + o.n = 4; + return o.tag + ":" + before + "," + o.n; +} + +function getterOnly() { + let i = 0; + let o = { get next() { return i++; } }; + return o.next + "," + o.next + "," + o.next; +} + +function setterOnly() { + let log = []; + let o = { set sink(v) { log.push(v); } }; + o.sink = "a"; + o.sink = "b"; + return log.join(",") + "/" + o.sink; +} + +function mixedOrder() { + let o = { + a: 1, + get b() { return this.a + 10; }, + c: 2, + set b(v) { this.a = v; }, + d: 3, + }; + let r1 = o.b; + o.b = 100; + return r1 + "," + o.b + "," + o.c + "," + o.d; +} + +console.log(pair()); +console.log(getterOnly()); +console.log(setterOnly()); +console.log(mixedOrder()); diff --git a/test/eir-arrowthis1.js b/test/eir-arrowthis1.js new file mode 100644 index 00000000..af608543 --- /dev/null +++ b/test/eir-arrowthis1.js @@ -0,0 +1,87 @@ +// arrow lexical `this` through the EIR pipeline: arrows read the owner +// function's captured this binding via the env chain + +function methodArrow() { + let o = { + tag: "T", + collect: function (xs) { + return xs.map((x) => this.tag + ":" + x).join(","); + }, + }; + return o.collect(["a", "b"]); +} + +function nestedArrows() { + let o = { + n: 5, + make: function () { + return () => () => this.n * 2; + }, + }; + return o.make()()(); +} + +function mixedCapture(prefix) { + let o = { + base: "B", + run: function (k) { + let local = k + 1; + let f = () => prefix + this.base + local; + return f(); + }, + }; + return o.run(1); +} + +function detachedArrow() { + let o = { + who: "owner", + getArrow: function () { + return () => this.who; + }, + }; + let f = o.getArrow(); + let other = { who: "other", f: f }; + // the arrow keeps its lexical this even called as a method of `other` + return other.f(); +} + +function genMethodThis() { + class Range { + constructor(n) { this.n = n; } + *items() { for (let i = 0; i < this.n; i++) yield i; } + } + let out = []; + for (let v of new Range(3).items()) out.push(v); + return out.join(","); +} + +function ctorArrow() { + class A { constructor(x) { this.x = x; } } + class B extends A { + constructor(x) { + super(x); + this.get = () => this.x + 1; + } + } + return new B(41).get(); +} + +function arrowArguments() { + let o = { + m: function () { + let f = () => arguments[0] + "/" + this.k; + return f("ignored"); + }, + k: "K", + }; + return o.m("outer"); +} + +console.log(methodArrow()); +console.log(nestedArrows()); +console.log(mixedCapture("p:")); +console.log(detachedArrow()); +console.log(genMethodThis()); +console.log(ctorArrow()); +console.log(arrowArguments()); diff --git a/test/eir-class1.js b/test/eir-class1.js new file mode 100644 index 00000000..1b72eb4d --- /dev/null +++ b/test/eir-class1.js @@ -0,0 +1,61 @@ +// classes through the EIR pipeline (DesugarClasses runs pre-EIR; the +// class intrinsics lower via lib/eir/intrinsics.js) + +function basics() { + class P { + constructor(x) { this.x = x; } + val() { return this.x; } + static tag() { return "P!"; } + } + let p = new P(7); + return p.val() + "/" + P.tag(); +} + +function derived(v) { + class A { + constructor(x) { this.x = x; } + describe() { return "A(" + this.x + ")"; } + } + class B extends A { + constructor(x) { super(x + 1); this.v = v; } + describe() { return "B[" + super.describe() + "," + this.v + "]"; } + } + let b = new B(10); + return b.describe() + " " + (b instanceof A) + (b instanceof B); +} + +function defaultCtor() { + class A { constructor() { this.who = "A"; } hi() { return "hi " + this.who; } } + class B extends A {} + return new B().hi(); +} + +function accessors() { + class T { + constructor() { this._n = 1; } + get n() { return this._n * 10; } + set n(v) { this._n = v + 1; } + } + let t = new T(); + let before = t.n; + t.n = 4; + return before + "," + t.n; +} + +function classExpr(k) { + let C = class { constructor() { this.k = k; } }; + return new C().k; +} + +function superSpread() { + class A { constructor(a, b, c) { this.sum = a + b + c; } } + class B extends A { constructor(xs) { super(...xs); } } + return new B([1, 2, 3]).sum; +} + +console.log(basics()); +console.log(derived("z")); +console.log(defaultCtor()); +console.log(accessors()); +console.log(classExpr("kk")); +console.log(superSpread()); diff --git a/test/eir-destructure1.js b/test/eir-destructure1.js new file mode 100644 index 00000000..3a0f4e20 --- /dev/null +++ b/test/eir-destructure1.js @@ -0,0 +1,62 @@ +// destructuring through the EIR pipeline (the first DesugarDestructuring +// run happens pre-EIR; %createIteratorWrapper lowers as a runtime call) + +function objPattern(o) { + let { a, b: renamed } = o; + return a + "," + renamed; +} + +function nestedPattern(o) { + let { x: { y }, z } = o; + return y + "," + z; +} + +function arrayPattern(xs) { + let [p, , q] = xs; + return p + "," + q; +} + +function arrayRest(xs) { + let [head, ...tail] = xs; + return head + "/" + tail.join("+"); +} + +function patternDefaults(o) { + // AssignmentPattern in patterns: panicked the whole compiler before + let { a = 10, b = 20 } = o; + let [c = 30, d = 40] = o.arr; + return [a, b, c, d].join(","); +} + +function nestedDefault(o) { + let { pos: { x = 1, y = 2 } = {} } = o; + return x + "," + y; +} + +function paramPattern({ a, b }, [c]) { + return a + b + c; +} + +function assignPosition(o) { + let a, b; + ({ a, b } = o); + let c, d; + [c, d] = [b, a]; + return a + "," + b + "/" + c + "," + d; +} + +function swap(x, y) { + [x, y] = [y, x]; + return x + "," + y; +} + +console.log(objPattern({ a: 1, b: 2 })); +console.log(nestedPattern({ x: { y: "Y" }, z: "Z" })); +console.log(arrayPattern(["p", "skip", "q"])); +console.log(arrayRest([1, 2, 3, 4])); +console.log(patternDefaults({ b: 99, arr: [undefined, 44] })); +console.log(nestedDefault({})); +console.log(nestedDefault({ pos: { x: 7 } })); +console.log(paramPattern({ a: 1, b: 2 }, [3])); +console.log(assignPosition({ a: "A", b: "B" })); +console.log(swap("l", "r")); diff --git a/test/eir-destructure2.js b/test/eir-destructure2.js new file mode 100644 index 00000000..afb30cb6 --- /dev/null +++ b/test/eir-destructure2.js @@ -0,0 +1,70 @@ +// pattern (and member-expression) loop heads, catch-parameter patterns, +// nested spreads, debugger statements: constructs that used to fall back +// to the legacy pipeline and now lower natively. + +function forOfArrayPattern(ps) { + let r = 0; + for (let [a, b] of ps) r += a * b; + return r; +} + +function forOfObjectPattern(items) { + let names = []; + for (const { name, n } of items) names.push(`${name}:${n}`); + return names.join(","); +} + +function forOfPatternCapture(ps) { + // body-scoped lets are per-iteration: each closure sees its own a/b + let fns = []; + for (let [a, b] of ps) fns.push(() => a + b); + return fns.map((f) => f()).join(","); +} + +function forOfMemberTarget(xs) { + let o = { last: null, seen: [] }; + for (o.last of xs) o.seen.push(o.last); + return `${o.seen.join("-")}|${o.last}`; +} + +function forInPattern(obj) { + let ks = []; + for (const k in obj) ks.push(k); + return ks.sort().join(","); +} + +function forOfAssignmentPattern(ps) { + let a, b; + let sums = []; + for ([a, b] of ps) sums.push(a + b); + return sums.join(","); +} + +function catchPattern(f) { + try { + f(); + return "no throw"; + } catch ({ message, code = 42 }) { + return `${message}/${code}`; + } +} + +function nestedSpread(xs) { + return [...[...xs, 5], 6]; +} + +function debuggerNoop(x) { + debugger; + return x + 1; +} + +console.log(forOfArrayPattern([[1, 2], [3, 4]])); +console.log(forOfObjectPattern([{ name: "a", n: 1 }, { name: "b", n: 2 }])); +console.log(forOfPatternCapture([[1, 2], [30, 4]])); +console.log(forOfMemberTarget(["x", "y", "z"])); +console.log(forInPattern({ q: 1, r: 2 })); +console.log(forOfAssignmentPattern([[1, 1], [2, 3]])); +console.log(catchPattern(() => { throw new Error("boom"); })); +console.log(catchPattern(() => 0)); +console.log(nestedSpread([1, 2]).join(" ")); +console.log(debuggerNoop(9)); diff --git a/test/eir-export1-lib.js b/test/eir-export1-lib.js new file mode 100644 index 00000000..bfc9b314 --- /dev/null +++ b/test/eir-export1-lib.js @@ -0,0 +1,14 @@ +export default class Counter { + constructor(n) { + this.n = n; + } + inc() { + return ++this.n; + } +} + +export const K = 7; + +export function mk() { + return "mk"; +} diff --git a/test/eir-export1.js b/test/eir-export1.js new file mode 100644 index 00000000..894bd275 --- /dev/null +++ b/test/eir-export1.js @@ -0,0 +1,10 @@ +// generator: esm +// `export default class`, default+named import, and re-export +import Counter, { K, mk } from "./eir-export1-lib"; +export { mk as remk } from "./eir-export1-lib"; + +let c = new Counter(K); +console.log(c.inc()); +console.log(c.inc()); +console.log(K); +console.log(mk()); diff --git a/test/eir-finally1.js b/test/eir-finally1.js new file mode 100644 index 00000000..54807cff --- /dev/null +++ b/test/eir-finally1.js @@ -0,0 +1,80 @@ +function order(g, log) { + try { + log.push("try"); + g(); + log.push("after"); + } finally { + log.push("finally"); + } + return log.join(","); +} + +function retThrough(v) { + let log = []; + function inner() { + try { + return "ret:" + v; + } finally { + log.push("fin"); + } + } + return inner() + "/" + log.join(","); +} + +function breakThrough(xs) { + let seen = []; + for (let i = 0; i < xs.length; i++) { + try { + if (xs[i] < 0) break; + seen.push(xs[i]); + } finally { + seen.push("f" + i); + } + } + return seen.join(","); +} + +function nested() { + let log = []; + function inner() { + try { + try { + return "v"; + } finally { + log.push("f1"); + } + } finally { + log.push("f2"); + } + } + return inner() + "/" + log.join(","); +} + +function override() { + try { + return "from-try"; + } finally { + return "from-finally"; + } +} + +function excPath(log) { + try { + try { + throw new Error("boom"); + } finally { + log.push("fin"); + } + } catch (e) { + log.push("caught:" + e.message); + } + return log.join(","); +} + +console.log(order(function () {}, [])); +try { console.log(order(function () { throw new Error("x"); }, [])); } catch (e) { console.log("threw"); } +console.log(retThrough(7)); +console.log(breakThrough([5, 6, -1, 9])); +console.log(nested()); +console.log(override()); +console.log(excPath([])); diff --git a/test/eir-generator1.js b/test/eir-generator1.js new file mode 100644 index 00000000..395f6258 --- /dev/null +++ b/test/eir-generator1.js @@ -0,0 +1,63 @@ +// generators through the EIR pipeline (DesugarGeneratorFunctions runs +// pre-EIR; coroutine-style — %makeGenerator/%generatorYield lower as +// runtime calls) + +function collect(g) { + let out = []; + for (let v of g) out.push(v); + return out.join(","); +} + +function basic() { + function* seq() { yield 1; yield 2; yield 3; } + return collect(seq()); +} + +function loopYield(n) { + function* upto() { for (let i = 0; i < n; i++) yield i * 10; } + return collect(upto()); +} + +function delegate() { + function* inner() { yield "b"; yield "c"; } + function* outer() { yield "a"; yield* inner(); yield "d"; } + return collect(outer()); +} + +function sentValues() { + function* echoing() { + let got = yield "first"; + let got2 = yield "got:" + got; + yield "got2:" + got2; + } + let g = echoing(); + let a = g.next().value; + let b = g.next("one").value; + let c = g.next("two").value; + return a + "/" + b + "/" + c; +} + +function doneProtocol() { + function* two() { yield 1; yield 2; } + let g = two(); + g.next(); g.next(); + let r = g.next(); + return r.done + "," + r.value; +} + +function genMethod() { + class Range { + constructor(n) { this.n = n; } + *items() { for (let i = 0; i < this.n; i++) yield i; } + } + // the desugared body is an arrow touching `this` -- exercises the + // class + generator pre-EIR combination even when it falls back + return collect(new Range(3).items()); +} + +console.log(basic()); +console.log(loopYield(4)); +console.log(delegate()); +console.log(sentValues()); +console.log(doneProtocol()); +console.log(genMethod()); diff --git a/test/eir-interop1-lib.js b/test/eir-interop1-lib.js new file mode 100644 index 00000000..03e46c1d --- /dev/null +++ b/test/eir-interop1-lib.js @@ -0,0 +1,29 @@ +export let counter = 0; +const K = 21; +export const NAME = "interop"; + +export function bump() { + counter = counter + 1; + return counter; +} + +export function doubled() { + return K * 2; +} + +export function helper(x) { + return x + 1; +} + +export function viaHelper() { + return helper(41); +} + +var fact = function (n) { + if (n < 2) return 1; + return n * fact(n - 1); +}; + +export function fact5() { + return fact(5); +} diff --git a/test/eir-interop1.js b/test/eir-interop1.js new file mode 100644 index 00000000..7ec51d78 --- /dev/null +++ b/test/eir-interop1.js @@ -0,0 +1,10 @@ +// generator: none +import { counter, NAME, bump, doubled, viaHelper, fact5 } from "./eir-interop1-lib"; + +console.log(NAME); +console.log(bump()); +console.log(bump()); +console.log(counter); +console.log(doubled()); +console.log(viaHelper()); +console.log(fact5()); diff --git a/test/eir-label1.js b/test/eir-label1.js new file mode 100644 index 00000000..a1002262 --- /dev/null +++ b/test/eir-label1.js @@ -0,0 +1,96 @@ +// labeled statements and labeled break/continue through EIR + +function labeledBreak(grid) { + let found = ""; + outer: for (let i = 0; i < grid.length; i++) { + for (let j = 0; j < grid[i].length; j++) { + if (grid[i][j] < 0) { found = i + "," + j; break outer; } + } + } + return found || "none"; +} + +function labeledContinue(n) { + let out = []; + outer: for (let i = 0; i < n; i++) { + for (let j = 0; j < n; j++) { + if (j > i) continue outer; + out.push(i + "" + j); + } + } + return out.join(","); +} + +function labeledBlock(x) { + let log = []; + done: { + log.push("a"); + if (x) break done; + log.push("b"); + } + log.push("c"); + return log.join(","); +} + +function labeledThroughFinally(xs) { + let log = []; + outer: for (let i = 0; i < xs.length; i++) { + try { + if (xs[i] < 0) break outer; + log.push("v" + xs[i]); + } finally { + log.push("f" + i); + } + } + return log.join(","); +} + +function labeledContinueThroughFinally(xs) { + let log = []; + outer: for (let i = 0; i < xs.length; i++) { + inner: for (let j = 0; j < 2; j++) { + try { + if (xs[i] < 0) continue outer; + log.push(i + ":" + j); + } finally { + log.push("f" + i + j); + } + } + } + return log.join(","); +} + +function labeledSwitch(k) { + let log = []; + pick: switch (k) { + case 1: + log.push("one"); + if (k === 1) break pick; + log.push("unreached"); + case 2: + log.push("two"); + } + log.push("after"); + return log.join(","); +} + +function labeledWhile(n) { + let c = 0; + again: while (true) { + c++; + if (c < n) continue again; + break again; + } + return c; +} + +console.log(labeledBreak([[1, 2], [3, -1], [5]])); +console.log(labeledBreak([[1], [2]])); +console.log(labeledContinue(3)); +console.log(labeledBlock(true)); +console.log(labeledBlock(false)); +console.log(labeledThroughFinally([7, -2, 9])); +console.log(labeledContinueThroughFinally([4, -5, 6])); +console.log(labeledSwitch(1)); +console.log(labeledSwitch(2)); +console.log(labeledWhile(4)); diff --git a/test/eir-loopenv1.js b/test/eir-loopenv1.js new file mode 100644 index 00000000..99907043 --- /dev/null +++ b/test/eir-loopenv1.js @@ -0,0 +1,53 @@ +// per-iteration environments: closures capturing let/const loop variables +// see their own iteration's binding (EIR loop envs; no DesugarLetLoopVars) + +function forCapture() { + let fns = []; + for (let i = 0; i < 3; i++) fns.push(function () { return i; }); + return fns.map(function (g) { return g(); }).join(","); +} +function forOfCapture(xs) { + let fns = []; + for (let x of xs) fns.push(function () { return x; }); + return fns.map(function (g) { return g(); }).join(","); +} +function forInCapture(o) { + let fns = []; + for (let k in o) fns.push(function () { return k; }); + return fns.map(function (g) { return g(); }).sort().join(","); +} +function mixedCapture(base) { + let fns = []; + for (let i = 0; i < 2; i++) { + for (let j = 0; j < 2; j++) fns.push(function () { return base + ":" + i + "" + j; }); + } + return fns.map(function (g) { return g(); }).join(" "); +} +function continueCapture(xs) { + let fns = []; + for (let i = 0; i < xs.length; i++) { + if (xs[i] < 0) continue; + fns.push(function () { return xs[i]; }); + } + return fns.map(function (g) { return g(); }).join(","); +} +function updateAfterCapture() { + let fns = []; + for (let i = 0; i < 3; i += 1) { + fns.push(function (d) { i = i + d; return i; }); + } + // each closure mutates its own iteration's binding + return fns.map(function (g) { return g(10); }).join(",") + "/" + fns.map(function (g) { return g(0); }).join(","); +} +function constForInCapture(o) { + let fns = []; + for (const k in o) fns.push(function () { return k; }); + return fns.map(function (g) { return g(); }).sort().join(","); +} +console.log(forCapture()); +console.log(forOfCapture(["a", "b", "c"])); +console.log(forInCapture({ p: 1, q: 2 })); +console.log(constForInCapture({ u: 1, v: 2 })); +console.log(mixedCapture("m")); +console.log(continueCapture([5, -1, 7])); +console.log(updateAfterCapture()); diff --git a/test/eir-lowtier1.js b/test/eir-lowtier1.js new file mode 100644 index 00000000..de71d5e4 --- /dev/null +++ b/test/eir-lowtier1.js @@ -0,0 +1,36 @@ +// Phase 2 low-tier probe. With -flowtier in the compiler's +// environment these function bodies are swapped for hand-built EIR +// (has_tag guard -> unbox/f64 op/box fast path vs the generic slow path; +// see lib/eir/lowtier-probe.ts). Without it they compile normally. +// Observable output must be identical either way. + +function lowtier_add(a, b) { return a + b; } +function lowtier_sub(a, b) { return a - b; } +function lowtier_mul(a, b) { return a * b; } +function lowtier_div(a, b) { return a / b; } +function lowtier_lt(a, b) { return a < b; } + +console.log(lowtier_add(2, 3)); // fast: 5 +console.log(lowtier_add(0.5, 0.25)); // fast: 0.75 +console.log(lowtier_add(NaN, 1)); // fast (NaN IS a number): NaN +console.log(lowtier_add(-0, 0)); // fast: 0 +console.log(lowtier_add(2147483647, 1)); // fast: 2147483648 +console.log(lowtier_add("a", "b")); // slow: ab +console.log(lowtier_add(2, "x")); // slow (mixed): 2x +console.log(lowtier_sub(5, 2)); // fast: 3 +console.log(lowtier_sub(0.75, 0.5)); // fast: 0.25 +console.log(lowtier_sub("5", 2)); // slow (string): 3 +console.log(lowtier_mul(3, 4)); // fast: 12 +console.log(lowtier_mul(-0.5, 4)); // fast: -2 +console.log(lowtier_mul("3", 4)); // slow (string): 12 +console.log(lowtier_div(1, 0)); // fast: Infinity (only a real fdiv does this) +console.log(lowtier_div(0, 0)); // fast: NaN +console.log(lowtier_div(7, 2)); // fast: 3.5 +// (no slow-path div row: the runtime's generic _ejs_op_div aborts on +// non-number operands — ejs-ops.c:901, pre-existing gap. Slow routing is +// the same parameterized diamond code path add/sub/mul exercise above.) +console.log(lowtier_lt(1, 2)); // fast: true +console.log(lowtier_lt(2, 1)); // fast: false +console.log(lowtier_lt(NaN, 1)); // fast: false +console.log(lowtier_lt(1, NaN)); // fast: false +console.log(lowtier_lt("a", "b")); // slow: true diff --git a/test/eir-newspread1.js b/test/eir-newspread1.js new file mode 100644 index 00000000..c864788f --- /dev/null +++ b/test/eir-newspread1.js @@ -0,0 +1,27 @@ +// new Foo(...args) — never compiled before (%constructApply) + +function Point(x, y, z) { this.sum = x + y + z; this.len = arguments.length; } + +function spreadNew(xs) { + let p = new Point(...xs); + return p.sum + "/" + p.len; +} + +function mixedNew(xs) { + let p = new Point(1, ...xs); + return p.sum + "/" + p.len; +} + +function litOnlyNew() { + let p = new Point(...[7, 8], 9); + return p.sum + "/" + p.len; +} + +class Tagged { constructor(...parts) { this.tag = parts.join("-"); } } +function classNew(xs) { return new Tagged(...xs, "end").tag; } + +console.log(spreadNew([1, 2, 3])); +console.log(mixedNew([10, 20])); +console.log(litOnlyNew()); +console.log(classNew(["a", "b"])); +console.log(new Point(...[4], 5, ...[6]) instanceof Point); diff --git a/test/eir-ns1-lib.js b/test/eir-ns1-lib.js new file mode 100644 index 00000000..df09b173 --- /dev/null +++ b/test/eir-ns1-lib.js @@ -0,0 +1,12 @@ +export function greet(name) { + return "hi " + name; +} +export const LIMIT = 10; +export let seen = 0; +export function bump() { + seen = seen + 1; + return seen; +} +export default function dfltFn(x) { + return x * 100; +} diff --git a/test/eir-ns1.js b/test/eir-ns1.js new file mode 100644 index 00000000..38f9cf4d --- /dev/null +++ b/test/eir-ns1.js @@ -0,0 +1,22 @@ +// generator: none +import * as lib from "./eir-ns1-lib"; +import dflt from "./eir-ns1-lib"; + +function useNs(name) { + let g = lib.greet(name); + return `${g}/${lib.LIMIT}`; +} + +function useNsState() { + lib.bump(); + lib.bump(); + return lib.seen; +} + +function useDefault(x) { + return dflt(x); +} + +console.log(useNs("eir")); +console.log(useNsState()); +console.log(useDefault(7)); diff --git a/test/eir-promo1-lib.js b/test/eir-promo1-lib.js new file mode 100644 index 00000000..7a484241 --- /dev/null +++ b/test/eir-promo1-lib.js @@ -0,0 +1,37 @@ +let counter = 0; +var state = { calls: 0 }; +const registry = []; + +var describe = function (tag) { + return `${tag}:${counter}:${state.calls}`; +}; + +function useAsValue(f, tag) { + return f(tag); +} + +export function tick() { + counter += 1; + state.calls++; + registry.push(counter); + return counter; +} + +export function readAll() { + return `${counter}/${state.calls}/${registry.join(",")}/${describe("r")}`; +} + +export function viaValue() { + return useAsValue(describe, "v"); +} + +// legacy-side interop: toplevel code (legacy) mutates the same storage +counter = 100; +state.calls = 50; + +export function makeCloser() { + return function () { + counter += 1000; + return counter; + }; +} diff --git a/test/eir-promo1.js b/test/eir-promo1.js new file mode 100644 index 00000000..657239b6 --- /dev/null +++ b/test/eir-promo1.js @@ -0,0 +1,10 @@ +// generator: none +import { tick, readAll, viaValue, makeCloser } from "./eir-promo1-lib"; + +console.log(tick()); +console.log(tick()); +console.log(readAll()); +console.log(viaValue()); +let c = makeCloser(); +console.log(c()); +console.log(readAll()); diff --git a/test/eir-recarrow1.js b/test/eir-recarrow1.js new file mode 100644 index 00000000..03f6b197 --- /dev/null +++ b/test/eir-recarrow1.js @@ -0,0 +1,34 @@ +function countdown() { + let walk = (n) => { + if (n <= 0) return 0; + return walk(n - 1) + 1; + }; + return walk(5); +} + +function namedRec() { + let visit = function (n) { + if (n === 0) return "done"; + return visit(n - 1); + }; + return visit(3); +} + +function walkTree(root) { + let seen = []; + let walk = (n) => { + if (!n || typeof n !== "object") return; + if (Array.isArray(n)) { + for (let el of n) walk(el); + return; + } + if (n.name) seen.push(n.name); + for (let k of Object.keys(n)) walk(n[k]); + }; + walk(root); + return seen.join(","); +} + +console.log(countdown()); +console.log(namedRec()); +console.log(walkTree({ name: "a", kids: [{ name: "b" }, { name: "c", kids: [{ name: "d" }] }] })); diff --git a/test/eir-spread1.js b/test/eir-spread1.js new file mode 100644 index 00000000..daa6bfb6 --- /dev/null +++ b/test/eir-spread1.js @@ -0,0 +1,43 @@ +// spread calls and spread array literals through the EIR pipeline +// (DesugarSpread runs pre-EIR; %arrayFromSpread lowers to +// array_from_spread) + +function join3(a, b, c) { + return a + "," + b + "," + c; +} + +function callSpread(xs) { + return join3(1, ...xs); +} + +function arraySpread(xs, ys) { + return [0, ...xs, 9, ...ys]; +} + +function methodSpread(xs) { + let o = { + base: "b", + m: function (x, y) { + return this.base + ":" + x + ":" + y; + }, + }; + return o.m(...xs); +} + +// a non-spread array-literal-with-spread argument next to a spread arg: +// the argument used to be silently dropped by DesugarSpread's bogus +// %arrayFromSpread flattening +function mixedArgs(xs, ys) { + return join3(...xs, [1, ...ys].join("+")); +} + +function nestedSpread(xs) { + return [...[...xs, 5], 6]; +} + +console.log(callSpread([2, 3])); +console.log(arraySpread([1, 2], [3]).join(" ")); +console.log(methodSpread(["x", "y"])); +console.log(mixedArgs([7, 8], [2, 3])); +console.log(nestedSpread([4]).join("")); +console.log(join3(...["t"], ...[], ...["u", "v"])); diff --git a/test/eir-syntax1.js b/test/eir-syntax1.js new file mode 100644 index 00000000..a2fc6b69 --- /dev/null +++ b/test/eir-syntax1.js @@ -0,0 +1,76 @@ +function counters() { + let i = 0; + let s = "5"; + console.log(i++); + console.log(++i); + console.log(s++); + console.log(--i); + let o = { n: 10, arr: [1, 2, 3] }; + o.n++; + o.arr[1]--; + console.log(o.n, o.arr[1]); +} + +function compounds() { + let x = 1; + x += 2; x *= 3; x -= 1; x %= 5; x <<= 2; x |= 1; x ^= 2; x >>= 1; + console.log(x); + let s = "a"; + s += "b"; + console.log(s); + let o = { v: 7 }; + o.v += 3; + o["v"] -= 1; + console.log(o.v); +} + +function templates(a, b) { + console.log(`plain`); + console.log(``.length); + console.log(`a=${a} b=${b}`); + console.log(`nested ${a > 1 ? `big ${a}` : "small"}!`); +} + +function switches(x) { + let r = ""; + switch (x) { + case 1: r += "one "; + case 2: r += "two "; break; + case 3: r += "three "; break; + default: r += "other "; + } + return r; +} + +function forofs(arr) { + let sum = 0; + for (let v of arr) { + if (v < 0) continue; + if (v > 99) break; + sum += v; + } + let last; + for (last of arr) {} + return `${sum}:${last}`; +} + +function withDefaults(a, b = a + 1, c = "x") { + return `${a},${b},${c}`; +} + +var doubler = (x) => x * 2; +var describe = (n) => { if (n % 2 === 0) return `even ${n}`; return `odd ${n}`; }; + +function arrows(arr) { + let big = arr.map(doubler).map((v) => v + 1); + console.log(big.join(",")); + console.log(describe(4), describe(5)); +} + +counters(); +compounds(); +templates(2, "z"); +console.log(switches(1), "|", switches(3), "|", switches(9)); +console.log(forofs([1, 2, -5, 3, 200, 4])); +console.log(withDefaults(1), "|", withDefaults(1, 5), "|", withDefaults(1, undefined, "y")); +arrows([1, 2, 3]); diff --git a/test/eir-syntax2-lib.js b/test/eir-syntax2-lib.js new file mode 100644 index 00000000..99833f0f --- /dev/null +++ b/test/eir-syntax2-lib.js @@ -0,0 +1,12 @@ +export function makeTag(name) { + return "<" + name + ">"; +} +export function twice(f, x) { + return f(f(x)); +} +export function inc(x) { + return x + 1; +} +export function incTwice(x) { + return twice(inc, x); +} diff --git a/test/eir-syntax2.js b/test/eir-syntax2.js new file mode 100644 index 00000000..64aac9ad --- /dev/null +++ b/test/eir-syntax2.js @@ -0,0 +1,37 @@ +// generator: none +import { makeTag, twice, inc, incTwice } from "./eir-syntax2-lib"; + +function forins(o) { + let ks = []; + for (let k in o) { + if (k === "skip") continue; + ks.push(k); + } + let k2; + for (k2 in o) {} + return ks.join(",") + "|" + k2; +} + +function rests(a, ...xs) { + return `${a}:${xs.length}:${xs.join("-")}`; +} + +function restOnly(...xs) { + return xs.map((x) => x * 2).join(","); +} + +function regexes(s) { + let re = /a(b+)c/i; + console.log(re.test(s)); + console.log(s.replace(/b+/g, "B")); + let m = s.match(/a(b+)c/); + console.log(m ? m[1] : "none"); +} + +console.log(forins({ x: 1, skip: 2, y: 3 })); +console.log(rests(9), "|", rests(9, 1), "|", rests(9, 1, 2, 3)); +console.log(restOnly(1, 2, 3)); +regexes("xxabbbcyy"); +console.log(twice(inc, 5)); +console.log(incTwice(10)); +console.log(makeTag("div")); diff --git a/test/eir-syntax4.js b/test/eir-syntax4.js new file mode 100644 index 00000000..c861a624 --- /dev/null +++ b/test/eir-syntax4.js @@ -0,0 +1,45 @@ +function argsLen() { + return arguments.length; +} + +function argsSum() { + let s = 0; + for (var i = 0; i < arguments.length; i++) s += arguments[i]; + return s; +} + +function argsArrow() { + let g = () => arguments.length + ":" + arguments[0]; + return g(); +} + +function objPat(o) { + // NOTE: no pattern defaults here — the legacy pipeline's + // DesugarDestructuring panics on AssignmentPattern (EIR supports + // them, but suite tests must pass both pipelines) + let { a, b: c } = o; + let d = o.d === undefined ? 9 : o.d; + return `${a}/${c}/${d}`; +} + +function delMember(o) { + delete o.x; + delete o["y"]; + return JSON.stringify(o); +} + +function afterInfinite(n) { + while (true) { + if (n > 2) break; + n++; + } + var node = n * 10; + return node; +} + +console.log(argsLen(), argsLen(1, 2, 3)); +console.log(argsSum(1, 2, 3, 4)); +console.log(argsArrow("x", "y")); +console.log(objPat({ a: 1, b: 2 }), "|", objPat({ a: 1, b: 2, d: 3 })); +console.log(delMember({ x: 1, y: 2, z: 3 })); +console.log(afterInfinite(0)); diff --git a/test/eir-tagged1.js b/test/eir-tagged1.js new file mode 100644 index 00000000..20299773 --- /dev/null +++ b/test/eir-tagged1.js @@ -0,0 +1,34 @@ +// tagged template literals through EIR (template_callsite) + +function tag(strings, ...subs) { + return strings.join("|") + "/" + strings.raw.join("|") + "/" + subs.join(","); +} + +function basic(x) { + return tag`a ${x} b ${x * 2} c`; +} + +let callsites = []; +function collect(strings) { callsites.push(strings); return "ok"; } +function identity(n) { + for (let i = 0; i < n; i++) collect`same site`; + return callsites.length === n && callsites.every(function (c) { return c === callsites[0]; }); +} + +let o = { + prefix: ">>", + m(strings, v) { return this.prefix + strings[0] + v; }, +}; +function methodTag(v) { + return o.m`lead ${v}`; +} + +function rawEscapes() { + return tag`x\n${1}`; +} + +console.log(basic(5)); +console.log(identity(3)); +console.log(methodTag(9)); +console.log(rawEscapes()); +console.log(tag`only literal`); diff --git a/test/eir-toplevel1.js b/test/eir-toplevel1.js new file mode 100644 index 00000000..239b8cc0 --- /dev/null +++ b/test/eir-toplevel1.js @@ -0,0 +1,34 @@ +// generator: esm + +// whole-module (toplevel-as-EIR) shapes: toplevel statements, captured +// toplevel locals, loop envs at toplevel, imports and exports. runs and +// must agree under --ir, --ir --ir-toplevel, and the legacy pipeline. + +import dflt, { K, inc, peek, counter } from "./eir-toplevel1/lib1"; +import * as lib from "./eir-toplevel1/lib1"; +import "./eir-toplevel1/lib1"; + +let greeting = "hello"; +var count = 0; +function bump(n) { count += n; return count; } +console.log(greeting.length); +console.log(bump(2) + "," + bump(3)); + +let fns = []; +for (let i = 0; i < 3; i++) fns.push(function () { return i; }); +console.log(fns.map(function (g) { return g(); }).join(",")); + +let holes = [, , "x"]; +let seen = 0; +holes.forEach(function () { seen++; }); +console.log(seen + "/" + holes.length + "/" + holes[2]); + +console.log(K); +console.log(inc(2) + "," + inc(3)); +console.log(peek()); +console.log(lib.K + "/" + lib.peek()); +console.log(dflt); +console.log(typeof bump === "function" ? bump.name : "?"); +let local = K * 2; +export { local as doubled }; +console.log(local); diff --git a/test/eir-toplevel1/lib1.js b/test/eir-toplevel1/lib1.js new file mode 100644 index 00000000..b7bdde45 --- /dev/null +++ b/test/eir-toplevel1/lib1.js @@ -0,0 +1,6 @@ +export const K = 7; +export let counter = 0; +export function inc(n) { counter += n; return counter; } +let hidden = "h"; +export function peek() { return hidden + K; } +export default "DFLT"; diff --git a/test/esprima-roundtrip1.js b/test/esprima-roundtrip1.js index f97aa4e4..e59370b9 100644 --- a/test/esprima-roundtrip1.js +++ b/test/esprima-roundtrip1.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm // skip-if: true // revisit the esprima tests now that we have the es6 modules diff --git a/test/esprima-roundtrip2.js b/test/esprima-roundtrip2.js index a420bd6f..fed6ada6 100644 --- a/test/esprima-roundtrip2.js +++ b/test/esprima-roundtrip2.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm // skip-if: true // revisit the esprima tests now that we have the es6 modules diff --git a/test/esprima1.js b/test/esprima1.js index 8bafe083..128cb391 100644 --- a/test/esprima1.js +++ b/test/esprima1.js @@ -1,4 +1,8 @@ -// generator: babel-node +// generator: none +// baseline checked in: the esm generator's transpile closure doesn't +// reach outside test/ (the ../external-deps esprima-es6 import; was +// silently unregenerable under babel-node and the old harness too); +// the output is JSON.stringify of the AST, engine-neutral // revisit the esprima tests now that we have the es6 modules import * as esprima from "../external-deps/esprima/esprima-es6"; diff --git a/test/expected/arguments6.js.expected-out b/test/expected/arguments6.js.expected-out new file mode 100644 index 00000000..e3bff4d6 --- /dev/null +++ b/test/expected/arguments6.js.expected-out @@ -0,0 +1,5 @@ +1,2,3 +9 +42 true +function +5,, diff --git a/test/expected/argv1.js.expected-out b/test/expected/argv1.js.expected-out index 7cd18e26..7863639e 100644 --- a/test/expected/argv1.js.expected-out +++ b/test/expected/argv1.js.expected-out @@ -1,3 +1,4 @@ -length = 2 -argv[0] = node -argv[1] = /Users/toshok/src/coffeekit/echo-js/test/argv1.js +length = 3 +argv[0] = /Users/toshok/.local/share/mise/installs/node/22.4.0/bin/node +argv[1] = /private/tmp/claude-501/-Users-toshok-src-echojs-echojs/da67aa89-c001-4316-943b-80ac66784ae0/scratchpad/p73-tree/test/harness-run.js +argv[2] = argv1.js diff --git a/test/expected/array21.js.expected-out b/test/expected/array21.js.expected-out index 765e7910..77a6562a 100644 --- a/test/expected/array21.js.expected-out +++ b/test/expected/array21.js.expected-out @@ -1,2 +1,2 @@ -[ 1, 2, 3, 4, 5, , , , , 6 ] +[ 1, 2, 3, 4, 5, <4 empty items>, 6 ] 10 diff --git a/test/expected/class-anon.js.expected-out b/test/expected/class-anon.js.expected-out deleted file mode 100644 index e50a49f9..00000000 --- a/test/expected/class-anon.js.expected-out +++ /dev/null @@ -1 +0,0 @@ -bar! diff --git a/test/expected/class-blockscoped.js.expected-out b/test/expected/class-blockscoped.js.expected-out deleted file mode 100644 index 27ba77dd..00000000 --- a/test/expected/class-blockscoped.js.expected-out +++ /dev/null @@ -1 +0,0 @@ -true diff --git a/test/expected/class-in-extends.js.expected-out b/test/expected/class-in-extends.js.expected-out deleted file mode 100644 index 27ba77dd..00000000 --- a/test/expected/class-in-extends.js.expected-out +++ /dev/null @@ -1 +0,0 @@ -true diff --git a/test/expected/date3.js.expected-out b/test/expected/date3.js.expected-out index b66115db..0e1c6b73 100644 --- a/test/expected/date3.js.expected-out +++ b/test/expected/date3.js.expected-out @@ -1,2 +1,2 @@ -Fri Sep 01 2000 00:00:00 GMT-0700 (PDT) -Sat Jan 01 2000 00:00:00 GMT-0800 (PST) +2000-09-01T00:00:00.000Z +2000-01-01T00:00:00.000Z diff --git a/test/expected/eir-accessor1.js.expected-out b/test/expected/eir-accessor1.js.expected-out new file mode 100644 index 00000000..c4de67db --- /dev/null +++ b/test/expected/eir-accessor1.js.expected-out @@ -0,0 +1,4 @@ +t:50,50 +0,1,2 +a,b/undefined +11,110,2,3 diff --git a/test/expected/eir-arrowthis1.js.expected-out b/test/expected/eir-arrowthis1.js.expected-out new file mode 100644 index 00000000..6b8165b5 --- /dev/null +++ b/test/expected/eir-arrowthis1.js.expected-out @@ -0,0 +1,7 @@ +T:a,T:b +10 +p:B2 +owner +0,1,2 +42 +outer/K diff --git a/test/expected/eir-class1.js.expected-out b/test/expected/eir-class1.js.expected-out new file mode 100644 index 00000000..a184a94b --- /dev/null +++ b/test/expected/eir-class1.js.expected-out @@ -0,0 +1,6 @@ +7/P! +B[A(11),z] truetrue +hi A +10,50 +kk +6 diff --git a/test/expected/eir-destructure1.js.expected-out b/test/expected/eir-destructure1.js.expected-out new file mode 100644 index 00000000..a86b81a9 --- /dev/null +++ b/test/expected/eir-destructure1.js.expected-out @@ -0,0 +1,10 @@ +1,2 +Y,Z +p,q +1/2+3+4 +10,99,30,44 +1,2 +7,2 +6 +A,B/B,A +r,l diff --git a/test/expected/eir-destructure2.js.expected-out b/test/expected/eir-destructure2.js.expected-out new file mode 100644 index 00000000..31d57f70 --- /dev/null +++ b/test/expected/eir-destructure2.js.expected-out @@ -0,0 +1,10 @@ +14 +a:1,b:2 +3,34 +x-y-z|z +q,r +2,5 +boom/42 +no throw +1 2 5 6 +10 diff --git a/test/expected/eir-export1.js.expected-out b/test/expected/eir-export1.js.expected-out new file mode 100644 index 00000000..a209119a --- /dev/null +++ b/test/expected/eir-export1.js.expected-out @@ -0,0 +1,4 @@ +8 +9 +7 +mk diff --git a/test/expected/eir-finally1.js.expected-out b/test/expected/eir-finally1.js.expected-out new file mode 100644 index 00000000..1779cc1f --- /dev/null +++ b/test/expected/eir-finally1.js.expected-out @@ -0,0 +1,7 @@ +try,after,finally +threw +ret:7/fin +5,f0,6,f1,f2 +v/f1,f2 +from-finally +fin,caught:boom diff --git a/test/expected/eir-generator1.js.expected-out b/test/expected/eir-generator1.js.expected-out new file mode 100644 index 00000000..fb6fecb8 --- /dev/null +++ b/test/expected/eir-generator1.js.expected-out @@ -0,0 +1,6 @@ +1,2,3 +0,10,20,30 +a,b,c,d +first/got:one/got2:two +true,undefined +0,1,2 diff --git a/test/expected/eir-interop1.js.expected-out b/test/expected/eir-interop1.js.expected-out new file mode 100644 index 00000000..e1760907 --- /dev/null +++ b/test/expected/eir-interop1.js.expected-out @@ -0,0 +1,7 @@ +interop +1 +2 +2 +42 +42 +120 diff --git a/test/expected/eir-label1.js.expected-out b/test/expected/eir-label1.js.expected-out new file mode 100644 index 00000000..71384466 --- /dev/null +++ b/test/expected/eir-label1.js.expected-out @@ -0,0 +1,10 @@ +1,1 +none +00,10,11,20,21,22 +a,c +a,b,c +v7,f0,f1 +0:0,f00,0:1,f01,f10,2:0,f20,2:1,f21 +one,after +two,after +4 diff --git a/test/expected/eir-loopenv1.js.expected-out b/test/expected/eir-loopenv1.js.expected-out new file mode 100644 index 00000000..d2efd4e5 --- /dev/null +++ b/test/expected/eir-loopenv1.js.expected-out @@ -0,0 +1,7 @@ +0,1,2 +a,b,c +p,q +u,v +m:00 m:01 m:10 m:11 +5,7 +10,11,12/10,11,12 diff --git a/test/expected/eir-lowtier1.js.expected-out b/test/expected/eir-lowtier1.js.expected-out new file mode 100644 index 00000000..a096e3f2 --- /dev/null +++ b/test/expected/eir-lowtier1.js.expected-out @@ -0,0 +1,21 @@ +5 +0.75 +NaN +0 +2147483648 +ab +2x +3 +0.25 +3 +12 +-2 +12 +Infinity +NaN +3.5 +true +false +false +false +true diff --git a/test/expected/eir-newspread1.js.expected-out b/test/expected/eir-newspread1.js.expected-out new file mode 100644 index 00000000..bdb598a8 --- /dev/null +++ b/test/expected/eir-newspread1.js.expected-out @@ -0,0 +1,5 @@ +6/3 +31/3 +24/3 +a-b-end +true diff --git a/test/expected/eir-ns1.js.expected-out b/test/expected/eir-ns1.js.expected-out new file mode 100644 index 00000000..e43fd73d --- /dev/null +++ b/test/expected/eir-ns1.js.expected-out @@ -0,0 +1,3 @@ +hi eir/10 +2 +700 diff --git a/test/expected/eir-promo1.js.expected-out b/test/expected/eir-promo1.js.expected-out new file mode 100644 index 00000000..c608f1a8 --- /dev/null +++ b/test/expected/eir-promo1.js.expected-out @@ -0,0 +1,6 @@ +101 +102 +102/52/101,102/r:102:52 +v:102:52 +1102 +1102/52/101,102/r:1102:52 diff --git a/test/expected/eir-recarrow1.js.expected-out b/test/expected/eir-recarrow1.js.expected-out new file mode 100644 index 00000000..219f401e --- /dev/null +++ b/test/expected/eir-recarrow1.js.expected-out @@ -0,0 +1,3 @@ +5 +done +a,b,c,d diff --git a/test/expected/eir-spread1.js.expected-out b/test/expected/eir-spread1.js.expected-out new file mode 100644 index 00000000..95c58b38 --- /dev/null +++ b/test/expected/eir-spread1.js.expected-out @@ -0,0 +1,6 @@ +1,2,3 +0 1 2 9 3 +b:x:y +7,8,1+2+3 +456 +t,u,v diff --git a/test/expected/eir-syntax1.js.expected-out b/test/expected/eir-syntax1.js.expected-out new file mode 100644 index 00000000..da1f3b9d --- /dev/null +++ b/test/expected/eir-syntax1.js.expected-out @@ -0,0 +1,17 @@ +0 +2 +5 +1 +11 1 +7 +ab +9 +plain +0 +a=2 b=z +nested big 2! +one two | three | other +6:4 +1,2,x | 1,5,x | 1,2,y +3,5,7 +even 4 odd 5 diff --git a/test/expected/eir-syntax2.js.expected-out b/test/expected/eir-syntax2.js.expected-out new file mode 100644 index 00000000..400d4643 --- /dev/null +++ b/test/expected/eir-syntax2.js.expected-out @@ -0,0 +1,9 @@ +x,y|y +9:0: | 9:1:1 | 9:3:1-2-3 +2,4,6 +true +xxaBcyy +bbb +7 +12 +
diff --git a/test/expected/eir-syntax4.js.expected-out b/test/expected/eir-syntax4.js.expected-out new file mode 100644 index 00000000..025817ac --- /dev/null +++ b/test/expected/eir-syntax4.js.expected-out @@ -0,0 +1,6 @@ +0 3 +10 +2:x +1/2/9 | 1/2/3 +{"z":3} +30 diff --git a/test/expected/eir-tagged1.js.expected-out b/test/expected/eir-tagged1.js.expected-out new file mode 100644 index 00000000..10961efa --- /dev/null +++ b/test/expected/eir-tagged1.js.expected-out @@ -0,0 +1,6 @@ +a | b | c/a | b | c/5,10 +true +>>lead 9 +x +|/x\n|/1 +only literal/only literal/ diff --git a/test/expected/eir-toplevel1.js.expected-out b/test/expected/eir-toplevel1.js.expected-out new file mode 100644 index 00000000..5d03f700 --- /dev/null +++ b/test/expected/eir-toplevel1.js.expected-out @@ -0,0 +1,11 @@ +5 +2,5 +0,1,2 +1/3/x +7 +2,5 +h7 +7/h7 +DFLT +bump +14 diff --git a/test/expected/finallythrow1.js.expected-out b/test/expected/finallythrow1.js.expected-out new file mode 100644 index 00000000..5e0891c5 --- /dev/null +++ b/test/expected/finallythrow1.js.expected-out @@ -0,0 +1,8 @@ +finally ran +caught: boom +caught: found:2 +caught: inner +5,f0,6,f1,f2 +fr ran +from-try +no throw diff --git a/test/expected/fundecl1.js.expected-out b/test/expected/fundecl1.js.expected-out index 58e07931..91156538 100644 --- a/test/expected/fundecl1.js.expected-out +++ b/test/expected/fundecl1.js.expected-out @@ -1,6 +1,6 @@ -bye -bye -bye +whu +hi +whu bye whu hi diff --git a/test/expected/gc-envwb1.js.expected-out b/test/expected/gc-envwb1.js.expected-out new file mode 100644 index 00000000..d81cc071 --- /dev/null +++ b/test/expected/gc-envwb1.js.expected-out @@ -0,0 +1 @@ +42 diff --git a/test/expected/gc-genstress1.js.expected-out b/test/expected/gc-genstress1.js.expected-out new file mode 100644 index 00000000..0d364ef2 --- /dev/null +++ b/test/expected/gc-genstress1.js.expected-out @@ -0,0 +1 @@ +0,50000,100000,150000,1900000 diff --git a/test/expected/gc-genstress2.js.expected-out b/test/expected/gc-genstress2.js.expected-out new file mode 100644 index 00000000..eb60c3fd --- /dev/null +++ b/test/expected/gc-genstress2.js.expected-out @@ -0,0 +1,3 @@ +0 +12348 +before diff --git a/test/expected/gc-ropes1.js.expected-out b/test/expected/gc-ropes1.js.expected-out new file mode 100644 index 00000000..b5ac53a9 --- /dev/null +++ b/test/expected/gc-ropes1.js.expected-out @@ -0,0 +1,3 @@ +190 +,x0,x1,x2,x3,x4,x5,x6,x7,x8,x9 +true diff --git a/test/expected/gc-ropes2.js.expected-out b/test/expected/gc-ropes2.js.expected-out new file mode 100644 index 00000000..ab4aa996 --- /dev/null +++ b/test/expected/gc-ropes2.js.expected-out @@ -0,0 +1,2 @@ +190 +,x0,x1,x2,x3,x4,x5,x6,x7,x8,x9 diff --git a/test/expected/gc5stress1.js.expected-out b/test/expected/gc5stress1.js.expected-out new file mode 100644 index 00000000..528485d6 --- /dev/null +++ b/test/expected/gc5stress1.js.expected-out @@ -0,0 +1,7 @@ +s1 3999000 +s2 749375 b123 e321 +s3 4912325 +s4 112350 r100 p,r p,q,r +s5 now-a-string0|0;1|x1;2|x2;now-a-string3|21;4|x4; +s6 one/two/three true false +s7 120200 diff --git a/test/expected/generator12.js.expected-out b/test/expected/generator12.js.expected-out deleted file mode 100644 index d00491fd..00000000 --- a/test/expected/generator12.js.expected-out +++ /dev/null @@ -1 +0,0 @@ -1 diff --git a/test/expected/generator22.js.expected-out b/test/expected/generator22.js.expected-out new file mode 100644 index 00000000..3e1c8d9f --- /dev/null +++ b/test/expected/generator22.js.expected-out @@ -0,0 +1,15 @@ +{"value":1,"done":false} +fin +{"value":5,"done":true} +{"done":true} +{"value":1,"done":false} +{"value":42,"done":true} +{"done":true} +{"value":9,"done":true} +{"done":true} +{"value":1,"done":false} +caught x +kfin +after +{"done":true} +caller caught early diff --git a/test/expected/generator23.js.expected-out b/test/expected/generator23.js.expected-out new file mode 100644 index 00000000..b0918d9b --- /dev/null +++ b/test/expected/generator23.js.expected-out @@ -0,0 +1 @@ +0,1000,2000,3000,78000 diff --git a/test/expected/generator24.js.expected-out b/test/expected/generator24.js.expected-out new file mode 100644 index 00000000..eb60c3fd --- /dev/null +++ b/test/expected/generator24.js.expected-out @@ -0,0 +1,3 @@ +0 +12348 +before diff --git a/test/expected/generator25.js.expected-out b/test/expected/generator25.js.expected-out new file mode 100644 index 00000000..83bc2e06 --- /dev/null +++ b/test/expected/generator25.js.expected-out @@ -0,0 +1,3 @@ +30 +in3 +10 diff --git a/test/expected/map6.js.expected-out b/test/expected/map6.js.expected-out new file mode 100644 index 00000000..eb11eb18 --- /dev/null +++ b/test/expected/map6.js.expected-out @@ -0,0 +1,9 @@ +true +false +2 +false +undefined +a=1,c=3 +3 +9 +a,c,b diff --git a/test/expected/math1.js.expected-out b/test/expected/math1.js.expected-out index 9283a0c3..77fa2825 100644 --- a/test/expected/math1.js.expected-out +++ b/test/expected/math1.js.expected-out @@ -7,14 +7,14 @@ NaN 10 -1 -1 -0 +-0 0 1 1 NaN -Infinity 1 -2.9999999999999996 +3 Infinity NaN NaN @@ -42,18 +42,18 @@ NaN NaN 0 1.3169578969248166 -0.8813735870195429 +0.881373587019543 0 NaN -Infinity 0 -0.5493061443340549 +0.5493061443340548 Infinity NaN 13 42 0 -0 +-0 -1 NaN NaN diff --git a/test/expected/module1.js.expected-out b/test/expected/module1.js.expected-out deleted file mode 100644 index 3b18e512..00000000 --- a/test/expected/module1.js.expected-out +++ /dev/null @@ -1 +0,0 @@ -hello world diff --git a/test/expected/number1.js.expected-out b/test/expected/number1.js.expected-out index 2c38d2fc..3726f983 100644 --- a/test/expected/number1.js.expected-out +++ b/test/expected/number1.js.expected-out @@ -1,3 +1,3 @@ 5 -{} +[Number: 5] 5 diff --git a/test/expected/object16.js.expected-out b/test/expected/object16.js.expected-out index ab3ea30f..16b607fc 100644 --- a/test/expected/object16.js.expected-out +++ b/test/expected/object16.js.expected-out @@ -3,6 +3,6 @@ 2 3 4 -[Function] +[Function (anonymous)] hi undefined diff --git a/test/expected/object19.js.expected-out b/test/expected/object19.js.expected-out new file mode 100644 index 00000000..e7c89bf9 --- /dev/null +++ b/test/expected/object19.js.expected-out @@ -0,0 +1,4 @@ +1 9 +1 undefined +2 function +4 3 diff --git a/test/expected/object9.js.expected-out b/test/expected/object9.js.expected-out index 682f20c5..1a28fd8f 100644 --- a/test/expected/object9.js.expected-out +++ b/test/expected/object9.js.expected-out @@ -1,4 +1,4 @@ -[Function] +[Function: get] undefined false undefined diff --git a/test/expected/proxy6.js.expected-out b/test/expected/proxy6.js.expected-out index a16476b4..26508b6f 100644 --- a/test/expected/proxy6.js.expected-out +++ b/test/expected/proxy6.js.expected-out @@ -1,4 +1,4 @@ -[ Internet Explorer, Netscape ] -[ Firefox ] -[ Firefox, Chrome ] +[ 'Internet Explorer', 'Netscape' ] +[ 'Firefox' ] +[ 'Firefox', 'Chrome' ] Chrome diff --git a/test/expected/reexport1.js.expected-out b/test/expected/reexport1.js.expected-out new file mode 100644 index 00000000..a555d6f1 --- /dev/null +++ b/test/expected/reexport1.js.expected-out @@ -0,0 +1,3 @@ +hi! +3 +yo!! diff --git a/test/expected/regexp-flags.js.expected-out b/test/expected/regexp-flags.js.expected-out deleted file mode 100644 index 30b4824f..00000000 --- a/test/expected/regexp-flags.js.expected-out +++ /dev/null @@ -1,2 +0,0 @@ -gim - diff --git a/test/expected/regexp-flags1.js.expected-out b/test/expected/regexp-flags1.js.expected-out new file mode 100644 index 00000000..9fd6b5d5 --- /dev/null +++ b/test/expected/regexp-flags1.js.expected-out @@ -0,0 +1,6 @@ +xxx +true +a +B +AbC +true diff --git a/test/expected/shapes-storm1.js.expected-out b/test/expected/shapes-storm1.js.expected-out new file mode 100644 index 00000000..fdd48288 --- /dev/null +++ b/test/expected/shapes-storm1.js.expected-out @@ -0,0 +1,24 @@ +sum 22990 +del keys x,z +del keys2 x,z,y,w 42 true false +attr keys p,q 3 +attr names p,q +dp 7 8 k,m +acc 42 base,twice +desc 4.25 true true true true +idx zero x true 0,name,after +froz 1 undefined true false +seal 2 true +pe 2 undefined false +forin own1,own2,inherited +hasOwn true false true +assign {"t":0,"u":1,"v":"two"} +assign2 {"r":2,"s":3} +defprops 1 2 one,two +sym hidden visible,visible2 1 +wide 780 40 0 39 +churn 60 flip0 flip7 59 +json {"a":1,"b":[1,2,3],"c":{"d":"e"},"f":4} +pts 14850 +upd replaced z +pie true false [object Object] diff --git a/test/expected/shiftassign1.js.expected-out b/test/expected/shiftassign1.js.expected-out new file mode 100644 index 00000000..b9d9706f --- /dev/null +++ b/test/expected/shiftassign1.js.expected-out @@ -0,0 +1,6 @@ +13 +15 +7 +20 +5 +5 diff --git a/test/expected/slice-negative1.js.expected-out b/test/expected/slice-negative1.js.expected-out new file mode 100644 index 00000000..0f80b486 --- /dev/null +++ b/test/expected/slice-negative1.js.expected-out @@ -0,0 +1,6 @@ +1,2,3,4 +4,5 +2,3,4 +0 +1,2,3,4,5 +1,2,3,4,5 diff --git a/test/expected/symbol-new.js.expected-out b/test/expected/symbol-new.js.expected-out deleted file mode 100644 index d5e07596..00000000 --- a/test/expected/symbol-new.js.expected-out +++ /dev/null @@ -1 +0,0 @@ -succeed diff --git a/test/expected/symbol-object.js.expected-out b/test/expected/symbol-object.js.expected-out deleted file mode 100644 index 27ba77dd..00000000 --- a/test/expected/symbol-object.js.expected-out +++ /dev/null @@ -1 +0,0 @@ -true diff --git a/test/expected/symbol-string-convert.js.expected-out b/test/expected/symbol-string-convert.js.expected-out deleted file mode 100644 index 27ba77dd..00000000 --- a/test/expected/symbol-string-convert.js.expected-out +++ /dev/null @@ -1 +0,0 @@ -true diff --git a/test/expected/template-nested1.js.expected-out b/test/expected/template-nested1.js.expected-out new file mode 100644 index 00000000..7ab9ac8e --- /dev/null +++ b/test/expected/template-nested1.js.expected-out @@ -0,0 +1,2 @@ +(a!, b!) +xy2zw diff --git a/test/expected/toLocaleString3.js.expected-out b/test/expected/toLocaleString3.js.expected-out index 0449af53..ecd019f1 100644 --- a/test/expected/toLocaleString3.js.expected-out +++ b/test/expected/toLocaleString3.js.expected-out @@ -1 +1 @@ -1.2355,1.2,hi there,[object Object] +1.236,1.2,hi there,[object Object] diff --git a/test/expected/tostring5.js.expected-out b/test/expected/tostring5.js.expected-out index ee5705f5..60fffd18 100644 --- a/test/expected/tostring5.js.expected-out +++ b/test/expected/tostring5.js.expected-out @@ -1,12 +1 @@ date -Invalid Date -object date.proto.tostring -[TypeError: this is not a Date object.] -number -0 -string - -boolean -false -regexp -/(?:)/ diff --git a/test/expected/typedarray4.js.expected-out b/test/expected/typedarray4.js.expected-out index 86766d6e..98ac48e6 100644 --- a/test/expected/typedarray4.js.expected-out +++ b/test/expected/typedarray4.js.expected-out @@ -15,4 +15,4 @@ Int32Array (4, 2): byteOffset: 4 byteLength: 8 length: 2 -[RangeError: Length is out of range.] +[RangeError: Invalid typed array length: 4] diff --git a/test/expected/typedarray5.js.expected-out b/test/expected/typedarray5.js.expected-out index d19b0d1b..3d8f4443 100644 --- a/test/expected/typedarray5.js.expected-out +++ b/test/expected/typedarray5.js.expected-out @@ -12,5 +12,5 @@ byte offset: 0 byte length: 4 1 3 -111 -113 +5 +7 diff --git a/test/expected/typeof1.js.expected-out b/test/expected/typeof1.js.expected-out index 80bb7b58..c1a99152 100644 --- a/test/expected/typeof1.js.expected-out +++ b/test/expected/typeof1.js.expected-out @@ -1,5 +1,5 @@ undefined -null +object string number object diff --git a/test/expected/types-argsink1.js.expected-out b/test/expected/types-argsink1.js.expected-out new file mode 100644 index 00000000..a0a8561a --- /dev/null +++ b/test/expected/types-argsink1.js.expected-out @@ -0,0 +1,7 @@ +0 1 0 3 0 3 +0 1 3 +0:undefined 1:x +1,2,3 +0: 2:1|2 +3 +2 7 diff --git a/test/expected/types-flowsink1.js.expected-out b/test/expected/types-flowsink1.js.expected-out new file mode 100644 index 00000000..03fcc536 --- /dev/null +++ b/test/expected/types-flowsink1.js.expected-out @@ -0,0 +1,13 @@ +1 +2 +0:0 +45:10 +5,9 +true:42:2 +77 +1 2 false +0,1,2,3:false +1:3 +8 +true +42 diff --git a/test/expected/types-sink1.js.expected-out b/test/expected/types-sink1.js.expected-out new file mode 100644 index 00000000..ddfd63c4 --- /dev/null +++ b/test/expected/types-sink1.js.expected-out @@ -0,0 +1 @@ +40000000003 diff --git a/test/expected/types-sink2.js.expected-out b/test/expected/types-sink2.js.expected-out new file mode 100644 index 00000000..5caff40c --- /dev/null +++ b/test/expected/types-sink2.js.expected-out @@ -0,0 +1 @@ +10000 diff --git a/test/finallythrow1.js b/test/finallythrow1.js new file mode 100644 index 00000000..37b7021a --- /dev/null +++ b/test/finallythrow1.js @@ -0,0 +1,57 @@ +function finOnly() { + try { + throw new Error("boom"); + } finally { + console.log("finally ran"); + } + return "SWALLOWED"; +} + +function forLetThrow(items) { + for (let i = 0; i < items.length; i++) { + if (items[i] === 3) throw new Error("found:" + i); + } + return "no throw"; +} + +function nestedFinally() { + let order = []; + try { + try { + throw new Error("inner"); + } finally { + order.push("f1"); + } + } finally { + order.push("f2"); + } + return order.join(","); +} + +function finallyBreak(items) { + let seen = []; + for (let i = 0; i < items.length; i++) { + try { + if (items[i] < 0) break; + seen.push(items[i]); + } finally { + seen.push("f" + i); + } + } + return seen.join(","); +} + +function finallyReturn() { + try { + return "from-try"; + } finally { + console.log("fr ran"); + } +} + +try { console.log(finOnly()); } catch (e) { console.log("caught: " + e.message); } +try { console.log(forLetThrow([1, 2, 3])); } catch (e) { console.log("caught: " + e.message); } +try { console.log(nestedFinally()); } catch (e) { console.log("caught: " + e.message); } +console.log(finallyBreak([5, 6, -1, 7])); +console.log(finallyReturn()); +console.log(forLetThrow([1, 2])); diff --git a/test/for3.js b/test/for3.js index 242e28c5..b0e7bd80 100644 --- a/test/for3.js +++ b/test/for3.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm for (var i = 0; i < 5; i++) { let i_ = i; diff --git a/test/for5.js b/test/for5.js index 7a6b6816..91e51933 100644 --- a/test/for5.js +++ b/test/for5.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm var a = Array(5); for (var i = 0; i < 5; i++) { diff --git a/test/for6.js b/test/for6.js index 5d93e42c..cb893c3f 100644 --- a/test/for6.js +++ b/test/for6.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm var a = Array(5); for (let i = 0; i < 5; i++) { diff --git a/test/forof1.js b/test/forof1.js index f60153f4..01b9e3a6 100644 --- a/test/forof1.js +++ b/test/forof1.js @@ -1,3 +1,3 @@ -// generator: babel-node +// generator: esm for (var i of ["hello", "world"]) console.log(i); diff --git a/test/forof2.js b/test/forof2.js index c9a062db..78608b73 100644 --- a/test/forof2.js +++ b/test/forof2.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm function foo() {} foo.prototype[Symbol.iterator] = function () { diff --git a/test/function-overriding2.js b/test/function-overriding2.js index 9f028936..a5cbd731 100644 --- a/test/function-overriding2.js +++ b/test/function-overriding2.js @@ -1,4 +1,4 @@ -// babel-node doesn't hoist blocked scope functions +// node doesn't hoist block-scoped functions the way ejs does here // generator: none if (typeof console !== "undefined") var print = console.log; diff --git a/test/fundecl1.js b/test/fundecl1.js index a655036c..f387c985 100644 --- a/test/fundecl1.js +++ b/test/fundecl1.js @@ -1,3 +1,5 @@ +// xfail: block-level function declarations hoist with pre-ES6 web semantics (last decl wins at function entry); ES2015 Annex B.3.3 gives whu/hi/whu/bye. stale-baseline zombie flushed by runtime-P3 + /* if (typeof(console) === "undefined") { var console = { diff --git a/test/gc-envwb1.js b/test/gc-envwb1.js new file mode 100644 index 00000000..9fcaee43 --- /dev/null +++ b/test/gc-envwb1.js @@ -0,0 +1,10 @@ +function mk() { + var x = null; + return { set: function (v) { x = v; }, get: function () { return x; } }; +} +var c = mk(); +function churn(n) { var t = []; for (var i = 0; i < n; i++) t.push({ p: i }); return t.length; } +churn(2000); +c.set({ fresh: 42 }); +churn(2000); +console.log(c.get().fresh); diff --git a/test/gc-gennest.js b/test/gc-gennest.js new file mode 100644 index 00000000..4a6962e1 --- /dev/null +++ b/test/gc-gennest.js @@ -0,0 +1,24 @@ +// nested active generators: A's body drives B while both hold stack-only refs +function* inner(base) { + var box = { v: base * 10, tag: "in" + base }; + yield box.v; + yield box.tag; +} +function* outer() { + var mine = { w: 7, s: [1, 2, 3] }; + var it = inner(3); + yield it.next().value; // B active inside A + yield it.next().value; + yield mine.w + mine.s.length; +} +function churn(n) { + var t = 0; + for (var i = 0; i < n; i++) { var o = { p: i }; t += o.p % 3; } + return t; +} +var it = outer(); +console.log(it.next().value); +churn(6000); +console.log(it.next().value); +churn(6000); +console.log(it.next().value); diff --git a/test/gc-gens1small.js b/test/gc-gens1small.js new file mode 100644 index 00000000..567c1641 --- /dev/null +++ b/test/gc-gens1small.js @@ -0,0 +1,15 @@ +function* g() { + var keep = []; + for (var i = 0; i < 4000; i++) { + keep.push({ a: i, b: i + 1 }); + if (i % 1000 === 0) yield i; + } + var sum = 0; + for (var j = 0; j < keep.length; j += 100) sum += keep[j].a; + yield sum; +} +var it = g(); +var r = it.next(); +var out = []; +while (!r.done) { out.push(r.value); r = it.next(); } +console.log(out.join(",")); diff --git a/test/gc-gens2small.js b/test/gc-gens2small.js new file mode 100644 index 00000000..dad95ad6 --- /dev/null +++ b/test/gc-gens2small.js @@ -0,0 +1,18 @@ +function* h() { + var local = { x: 12345, s: "before" }; + var arr = [1, 2, 3]; + yield 0; + yield local.x + arr.length; + yield local.s; +} +function churn(n) { + var t = 0; + for (var i = 0; i < n; i++) { var o = { p: i, q: [i, i] }; t += o.p; } + return t; +} +var it = h(); +console.log(it.next().value); +churn(8000); +console.log(it.next().value); +churn(8000); +console.log(it.next().value); diff --git a/test/gc-genstress1.js b/test/gc-genstress1.js new file mode 100644 index 00000000..8003f850 --- /dev/null +++ b/test/gc-genstress1.js @@ -0,0 +1,16 @@ +// GC triggers while running ON the generator's stack +function* g() { + var keep = []; + for (var i = 0; i < 200000; i++) { + keep.push({ a: i, b: i + 1 }); + if (i % 50000 === 0) yield i; + } + var sum = 0; + for (var j = 0; j < keep.length; j += 10000) sum += keep[j].a; + yield sum; +} +var it = g(); +var r = it.next(); +var out = []; +while (!r.done) { out.push(r.value); r = it.next(); } +console.log(out.join(",")); diff --git a/test/gc-genstress2.js b/test/gc-genstress2.js new file mode 100644 index 00000000..01805380 --- /dev/null +++ b/test/gc-genstress2.js @@ -0,0 +1,20 @@ +// values whose ONLY reference lives in a suspended generator's stack +// frames, across GCs forced from the main stack +function* h() { + var local = { x: 12345, s: "before" }; + var arr = [1, 2, 3]; + yield 0; // suspend with local/arr live only here + yield local.x + arr.length; // use them after resumes+GCs + yield local.s; +} +function churn(n) { + var t = 0; + for (var i = 0; i < n; i++) { var o = { p: i, q: [i, i] }; t += o.p; } + return t; +} +var it = h(); +console.log(it.next().value); +churn(400000); // force collections while h is suspended +console.log(it.next().value); +churn(400000); +console.log(it.next().value); diff --git a/test/gc-ropes1.js b/test/gc-ropes1.js new file mode 100644 index 00000000..a7d5924f --- /dev/null +++ b/test/gc-ropes1.js @@ -0,0 +1,7 @@ +var parts = []; +for (var i = 0; i < 50; i++) parts.push("x" + i); +var s = ""; +for (var i = 0; i < 50; i++) s = s + "," + parts[i]; +console.log(s.length); +console.log(s.substring(0, 30)); +console.log(s === s.split("").join("")); diff --git a/test/gc-ropes2.js b/test/gc-ropes2.js new file mode 100644 index 00000000..b9509e2b --- /dev/null +++ b/test/gc-ropes2.js @@ -0,0 +1,10 @@ +function build() { + var parts = []; + for (var i = 0; i < 50; i++) parts.push("x" + i); + var s = ""; + for (var i = 0; i < 50; i++) s = s + "," + parts[i]; + return s; +} +var out = build(); +console.log(out.length); +console.log(out.substring(0, 30)); diff --git a/test/gc5stress1.js b/test/gc5stress1.js new file mode 100644 index 00000000..486c365a --- /dev/null +++ b/test/gc5stress1.js @@ -0,0 +1,104 @@ +// gc-P5 embedded-slot stress: single-cell born-with-shape objects, +// growth past embedded capacity, ctor birth-capacity hints, dictionary +// migration out of embedded storage, f64 slots + repr flips, and +// old->young barrier traffic with the object as owner. Run under +// EJS_GC_EVERY_N_ALLOC to force collections between every step. + +// 1. ctor hint: first construct mishinted (bare cell + out-of-line), +// subsequent constructs embedded. p.x/p.y arithmetic keeps values hot. +function Point(x, y) { + this.x = x; + this.y = y; +} +var pts = []; +var s = 0; +for (var i = 0; i < 2000; i++) { + var p = new Point(i, i + 0.5); + pts.push(p); + s += p.x + p.y; +} +console.log("s1", s); + +// 2. growth past embedded capacity: literal born with 2 fields, then 6 +// more appended (out-of-line degrade), values must survive moves. +var grown = []; +for (var i = 0; i < 500; i++) { + var o = { a: i, b: "b" + i }; + o.c = i * 2; + o.d = { nested: i }; + o.e = "e" + i; + o.f = i + 0.25; + o.g = [i, i + 1]; + o.h = i % 2 === 0; + grown.push(o); +} +var t = 0; +for (var i = 0; i < grown.length; i++) { + var o = grown[i]; + t += o.a + o.c + o.f + o.d.nested + o.g[1] + (o.h ? 1 : 0); +} +console.log("s2", t, grown[123].b, grown[321].e); + +// 3. old->young stores through shaped slots: long-lived receivers get +// freshly allocated values written into existing slots (the +// barrier-owner-flip path), across many collections. +var holders = []; +for (var i = 0; i < 100; i++) holders.push({ v: null, w: 0 }); +for (var round = 0; round < 50; round++) { + for (var i = 0; i < holders.length; i++) { + holders[i].v = { fresh: round * 1000 + i }; + holders[i].w = round + i / 2; + } +} +var u = 0; +for (var i = 0; i < holders.length; i++) u += holders[i].v.fresh + holders[i].w; +console.log("s3", u); + +// 4. dictionary migration out of embedded storage: delete a field, then +// keep using the object. +var migr = []; +for (var i = 0; i < 300; i++) { + var m = { p: i, q: i * 3, r: "r" + i }; + if (i % 2 === 0) delete m.q; + migr.push(m); +} +var v = 0; +for (var i = 0; i < migr.length; i++) { + v += migr[i].p + (migr[i].q === undefined ? 0 : migr[i].q); +} +console.log("s4", v, migr[100].r, Object.keys(migr[0]).join(","), Object.keys(migr[1]).join(",")); + +// 5. repr flips in embedded slots: number slot takes a string, string +// slot takes a number. +var flip = []; +for (var i = 0; i < 200; i++) { + var f = { n: i, s: "x" + i }; + if (i % 3 === 0) { f.n = "now-a-string" + i; f.s = i * 7; } + flip.push(f); +} +var w = ""; +for (var i = 0; i < 5; i++) w += flip[i].n + "|" + flip[i].s + ";"; +console.log("s5", w); + +// 6. enumeration order + in-operator on embedded objects. +var e = { one: 1, two: 2, three: 3 }; +var names = []; +for (var k in e) names.push(k); +console.log("s6", names.join("/"), "two" in e, "nope" in e); + +// 7. ctor that installs a growing number of fields (hint too small on +// later constructs). +function Growy(n) { + this.base = n; + if (n % 2 === 0) { + this.extra1 = n + 1; + this.extra2 = n + 2; + this.extra3 = n + 3; + } +} +var g = 0; +for (var i = 0; i < 400; i++) { + var gr = new Growy(i); + g += gr.base + (gr.extra3 === undefined ? 0 : gr.extra3); +} +console.log("s7", g); diff --git a/test/generator1.js b/test/generator1.js index da983423..87be8d0b 100644 --- a/test/generator1.js +++ b/test/generator1.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm // "basic functionality" from kangax function* generator() { diff --git a/test/generator10.js b/test/generator10.js index 19f0cbcb..829c0114 100644 --- a/test/generator10.js +++ b/test/generator10.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm // "yield *, arrays" from kangax var iterator = (function* generator() { diff --git a/test/generator11.js b/test/generator11.js index c1f9cebe..f9e66647 100644 --- a/test/generator11.js +++ b/test/generator11.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm // "yield *, strings" from kangax diff --git a/test/generator12.js b/test/generator12.js index 98ceab29..f4cc2892 100644 --- a/test/generator12.js +++ b/test/generator12.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm // skip-if: true // this test fails for stage1 but not for stage0. we need to add a way to disable tests just for particular stages diff --git a/test/generator13.js b/test/generator13.js index f30927dd..8667b838 100644 --- a/test/generator13.js +++ b/test/generator13.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm // "yield *, generic iterables" from kangax diff --git a/test/generator14.js b/test/generator14.js index c7a11613..8631dff9 100644 --- a/test/generator14.js +++ b/test/generator14.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm // "yield *, instances of iterables" from kangax diff --git a/test/generator15.js b/test/generator15.js index 06de9182..d6a76cb0 100644 --- a/test/generator15.js +++ b/test/generator15.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm // xfail: generator support isn't 100% // "yield *, iterator closing" from kangax diff --git a/test/generator16.js b/test/generator16.js index 99370782..a4f60766 100644 --- a/test/generator16.js +++ b/test/generator16.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm // xfail: generator support isn't 100% // "yield *, iterator closing via throw()" from kangax diff --git a/test/generator17.js b/test/generator17.js index 7812b074..2fd412ed 100644 --- a/test/generator17.js +++ b/test/generator17.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm // "shorthand generator methods" from kangax diff --git a/test/generator18.js b/test/generator18.js index 88cf33a7..66c53509 100644 --- a/test/generator18.js +++ b/test/generator18.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm // "string-keyed shorthand generator methods" from kangax var o = { diff --git a/test/generator19.js b/test/generator19.js index 75d83d21..c394d2f3 100644 --- a/test/generator19.js +++ b/test/generator19.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm // "computed shorthand generators" from kangax var garply = "generator"; diff --git a/test/generator2.js b/test/generator2.js index e3dc2129..eb11713a 100644 --- a/test/generator2.js +++ b/test/generator2.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm // "generator function expressions" from kangax diff --git a/test/generator20.js b/test/generator20.js index 349d5dd7..f287ad38 100644 --- a/test/generator20.js +++ b/test/generator20.js @@ -1,5 +1,4 @@ -// generator: babel-node -// xfail: generator support isn't 100% +// generator: esm // "shorthand generator methods, classes" from kangax diff --git a/test/generator21.js b/test/generator21.js index ce15470d..ea3ca7f1 100644 --- a/test/generator21.js +++ b/test/generator21.js @@ -1,5 +1,4 @@ -// generator: babel-node -// xfail: generator support isn't 100% +// generator: esm // "computed shorthand generators, classes" diff --git a/test/generator22.js b/test/generator22.js new file mode 100644 index 00000000..bbcb1e1f --- /dev/null +++ b/test/generator22.js @@ -0,0 +1,54 @@ +// generator.return(): finally blocks run, the return value lands in the +// final iteration result, and completed generators answer correctly. + +function* g() { + try { + yield 1; + yield 2; + } finally { + console.log("fin"); + } +} + +let it = g(); +console.log(JSON.stringify(it.next())); +console.log(JSON.stringify(it.return(5))); +console.log(JSON.stringify(it.next())); + +// the body's return value is the final result's value +function* h() { + yield 1; + return 42; +} +let it2 = h(); +console.log(JSON.stringify(it2.next())); +console.log(JSON.stringify(it2.next())); +console.log(JSON.stringify(it2.next())); + +// .return before the first next: the body never runs +let it3 = g(); +console.log(JSON.stringify(it3.return(9))); +console.log(JSON.stringify(it3.next())); + +// .throw resumes at the yield: catch and finally both run +function* k() { + try { + yield 1; + } catch (e) { + console.log("caught", e); + } finally { + console.log("kfin"); + } + console.log("after"); +} +let it4 = k(); +console.log(JSON.stringify(it4.next())); +console.log(JSON.stringify(it4.throw("x"))); + +// .throw at a never-started generator throws in the caller +let it5 = k(); +try { + it5.throw("early"); +} catch (e) { + console.log("caller caught", e); +} diff --git a/test/generator23.js b/test/generator23.js new file mode 100644 index 00000000..86b79e68 --- /dev/null +++ b/test/generator23.js @@ -0,0 +1,20 @@ +// gc-plan P0: a collection triggered while EXECUTING ON the generator's +// malloc'd stack (generator bodies call _ejs_gc_alloc). Before the P0 fix +// mark_thread_stack scanned [&local, main-stack-bottom) from the generator +// stack — a bogus range spanning unmapped memory: instant segfault under +// EJS_GC_EVERY_N_ALLOC=7, silent overscan otherwise. +function* g() { + var keep = []; + for (var i = 0; i < 4000; i++) { + keep.push({ a: i, b: i + 1 }); + if (i % 1000 === 0) yield i; + } + var sum = 0; + for (var j = 0; j < keep.length; j += 100) sum += keep[j].a; + yield sum; +} +var it = g(); +var r = it.next(); +var out = []; +while (!r.done) { out.push(r.value); r = it.next(); } +console.log(out.join(",")); diff --git a/test/generator24.js b/test/generator24.js new file mode 100644 index 00000000..53bc896b --- /dev/null +++ b/test/generator24.js @@ -0,0 +1,22 @@ +// gc-plan P0: values whose ONLY references live in a SUSPENDED +// generator's stack frames must survive collections forced from the main +// stack. Before the P0 fix the suspended-stack scan covered [stack, sp) +// — the dead region below the suspension point — missing every live frame. +function* h() { + var local = { x: 12345, s: "before" }; + var arr = [1, 2, 3]; + yield 0; + yield local.x + arr.length; + yield local.s; +} +function churn(n) { + var t = 0; + for (var i = 0; i < n; i++) { var o = { p: i, q: [i, i] }; t += o.p; } + return t; +} +var it = h(); +console.log(it.next().value); +churn(8000); +console.log(it.next().value); +churn(8000); +console.log(it.next().value); diff --git a/test/generator25.js b/test/generator25.js new file mode 100644 index 00000000..84a21a5e --- /dev/null +++ b/test/generator25.js @@ -0,0 +1,28 @@ +// gc-plan P0: NESTED active generators — the collector must cover the +// whole stack chain: the running stack, each suspended parent generator's +// segment, and the suspended main-stack segment behind the outermost +// resume site. +// nested active generators: A's body drives B while both hold stack-only refs +function* inner(base) { + var box = { v: base * 10, tag: "in" + base }; + yield box.v; + yield box.tag; +} +function* outer() { + var mine = { w: 7, s: [1, 2, 3] }; + var it = inner(3); + yield it.next().value; // B active inside A + yield it.next().value; + yield mine.w + mine.s.length; +} +function churn(n) { + var t = 0; + for (var i = 0; i < n; i++) { var o = { p: i }; t += o.p % 3; } + return t; +} +var it = outer(); +console.log(it.next().value); +churn(6000); +console.log(it.next().value); +churn(6000); +console.log(it.next().value); diff --git a/test/generator3.js b/test/generator3.js index 72102965..36572f93 100644 --- a/test/generator3.js +++ b/test/generator3.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm // "correct this binding" from kangax diff --git a/test/generator5.js b/test/generator5.js index 12d4430b..0c22e2fa 100644 --- a/test/generator5.js +++ b/test/generator5.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm // xfail: generator support isn't 100% // "sending" from kangax diff --git a/test/generator6.js b/test/generator6.js index 50a48103..91932a97 100644 --- a/test/generator6.js +++ b/test/generator6.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm // xfail: generator support isn't 100% // "%GeneratorPrototype%" from kangax diff --git a/test/generator7.js b/test/generator7.js index 9a3f393d..9ba7d913 100644 --- a/test/generator7.js +++ b/test/generator7.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm // "%GeneratorPrototype%.throw" from kangax var passed = false; diff --git a/test/generator8.js b/test/generator8.js index 11f457aa..5b577ab0 100644 --- a/test/generator8.js +++ b/test/generator8.js @@ -1,5 +1,4 @@ -// generator: babel-node -// xfail: generator support isn't 100% +// generator: esm // "%GeneratorPrototype%.return" from kangax diff --git a/test/generator9.js b/test/generator9.js index e1d81dd1..500e48cd 100644 --- a/test/generator9.js +++ b/test/generator9.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm // "yield operator precedence" from kangax var passed; diff --git a/test/harness-console-shim.js b/test/harness-console-shim.js new file mode 100644 index 00000000..6f3fbfba --- /dev/null +++ b/test/harness-console-shim.js @@ -0,0 +1,265 @@ +// harness-console-shim: the value-based test harness (runtime-P3). +// +// Replaces console.log/warn/error with a serializer OWNED BY THIS FILE. +// The same code runs under node (expected-output generation, via +// harness-run.js) and compiled into each test executable (via the import +// wrapper tester.js generates), so a test's baseline and its output agree +// iff the VALUES it logged agree — no engine's inspect format is in the +// loop, and node upgrades can't drift the baselines. +// +// Rules for editing this file: +// - conservative ES5 only: it must compile under ejs and run under +// node byte-identically (including from the esm generator's +// transpile dir, where it rides along unconverted); +// - no engine-provided formatting (util.inspect, toISOString, ...); +// anything observable must be computed here, from values; +// - it must not rely on ejs-specific or node-specific behavior: any +// asymmetry becomes a spurious diff in every test. +// +// Known engine gaps deliberately absorbed here (worked around, so the +// harness itself never trips them): +// - ejs Object.keys(array) omits index keys (node includes them): +// elements are walked by index, and index-shaped keys are filtered +// from the named-property pass on both engines; +// - ejs lacks Date.prototype.toISOString: the ISO string is computed +// from getTime() with civil-date math. + +(function () { + var origLog = console.log; + var origError = console.error; + + function isIdentChar(cc, first) { + if ((cc >= 65 && cc <= 90) || (cc >= 97 && cc <= 122) || cc === 95 || cc === 36) return true; + return !first && cc >= 48 && cc <= 57; + } + + function isIdentLike(s) { + if (s.length === 0) return false; + for (var i = 0; i < s.length; i++) { + if (!isIdentChar(s.charCodeAt(i), i === 0)) return false; + } + return true; + } + + function quoteString(s) { + var out = "'"; + for (var i = 0; i < s.length; i++) { + var cc = s.charCodeAt(i); + var ch = s.charAt(i); + if (ch === "'") out += "\\'"; + else if (ch === "\\") out += "\\\\"; + else if (ch === "\n") out += "\\n"; + else if (ch === "\r") out += "\\r"; + else if (ch === "\t") out += "\\t"; + else if (cc < 32) { + var hex = cc.toString(16); + if (hex.length < 2) hex = "0" + hex; + out += "\\x" + hex; + } else out += ch; + } + return out + "'"; + } + + function numberToString(n) { + if (n === 0 && 1 / n === -Infinity) return "-0"; + return String(n); + } + + function pad(n, w) { + var s = String(n); + while (s.length < w) s = "0" + s; + return s; + } + + // ISO-8601 from epoch millis; civil-from-days per Howard Hinnant. + function dateToISO(d) { + var t; + try { + t = d.getTime(); + } catch (e) { + t = NaN; + } + if (t !== t) return "Invalid Date"; + var ms = t % 86400000; + if (ms < 0) ms += 86400000; + var days = (t - ms) / 86400000; + var z = days + 719468; + var era = Math.floor(z / 146097); + var doe = z - era * 146097; + var yoe = Math.floor( + (doe - Math.floor(doe / 1460) + Math.floor(doe / 36524) - Math.floor(doe / 146096)) / + 365 + ); + var y = yoe + era * 400; + var doy = doe - (365 * yoe + Math.floor(yoe / 4) - Math.floor(yoe / 100)); + var mp = Math.floor((5 * doy + 2) / 153); + var day = doy - Math.floor((153 * mp + 2) / 5) + 1; + var m = mp < 10 ? mp + 3 : mp - 9; + if (m <= 2) y += 1; + var hh = Math.floor(ms / 3600000); + ms -= hh * 3600000; + var mm = Math.floor(ms / 60000); + ms -= mm * 60000; + var ss = Math.floor(ms / 1000); + ms -= ss * 1000; + return ( + pad(y, 4) + "-" + pad(m, 2) + "-" + pad(day, 2) + + "T" + pad(hh, 2) + ":" + pad(mm, 2) + ":" + pad(ss, 2) + "." + pad(ms, 3) + "Z" + ); + } + + // canonical non-negative-integer key, i.e. an array index that the + // element walk already covered + function isIndexKey(k) { + var n = Math.floor(Number(k)); + return n >= 0 && String(n) === k; + } + + function constructorName(v) { + try { + if (Object.getPrototypeOf && Object.getPrototypeOf(v) === null) return null; + var c = v.constructor; + if (typeof c === "function" && typeof c.name === "string" && c.name.length > 0) + return c.name; + } catch (e) {} + return ""; + } + + function fmtArrayBody(v, len, seen) { + var parts = []; + var emptyRun = 0; + for (var i = 0; i < len; i++) { + if (!(i in v)) { + emptyRun++; + continue; + } + if (emptyRun > 0) { + parts.push("<" + emptyRun + " empty item" + (emptyRun === 1 ? "" : "s") + ">"); + emptyRun = 0; + } + parts.push(fmt(v[i], seen)); + } + if (emptyRun > 0) + parts.push("<" + emptyRun + " empty item" + (emptyRun === 1 ? "" : "s") + ">"); + var keys = []; + try { + keys = Object.keys(v); + } catch (e) {} + for (var j = 0; j < keys.length; j++) { + var k = keys[j]; + if (k === "length" || isIndexKey(k)) continue; + parts.push((isIdentLike(k) ? k : quoteString(k)) + ": " + fmt(v[k], seen)); + } + if (parts.length === 0) return "[]"; + return "[ " + parts.join(", ") + " ]"; + } + + function fmtObject(v, seen) { + var prefix = ""; + var cn = constructorName(v); + if (cn === null) prefix = "[Object: null prototype] "; + else if (cn !== "" && cn !== "Object") prefix = cn + " "; + var keys = []; + try { + keys = Object.keys(v); + } catch (e) {} + if (keys.length === 0) return prefix + "{}"; + var parts = []; + for (var i = 0; i < keys.length; i++) { + var k = keys[i]; + parts.push((isIdentLike(k) ? k : quoteString(k)) + ": " + fmt(v[k], seen)); + } + return prefix + "{ " + parts.join(", ") + " }"; + } + + function fmt(v, seen) { + var t = typeof v; + if (v === null) return "null"; + if (t === "undefined") return "undefined"; + if (t === "number") return numberToString(v); + if (t === "boolean") return String(v); + if (t === "string") return quoteString(v); + if (t === "symbol") { + try { + return v.toString(); + } catch (e) { + return "Symbol(?)"; + } + } + if (t === "function") { + var fname = ""; + try { + fname = v.name; + } catch (e) {} + return fname ? "[Function: " + fname + "]" : "[Function (anonymous)]"; + } + + if (seen.indexOf(v) !== -1) return "[Circular]"; + seen.push(v); + var out; + try { + out = fmtNonPrimitive(v, seen); + } catch (e) { + out = "[unserializable: " + e + "]"; + } + seen.pop(); + return out; + } + + function fmtNonPrimitive(v, seen) { + if (Array.isArray(v)) return fmtArrayBody(v, v.length, seen); + if (v instanceof Error) { + var ename = v.name || "Error"; + return v.message ? "[" + ename + ": " + v.message + "]" : "[" + ename + "]"; + } + if (v instanceof Date) return dateToISO(v); + if (v instanceof RegExp) return String(v); + if (typeof Map === "function" && v instanceof Map) { + var mparts = []; + v.forEach(function (val, key) { + mparts.push(fmt(key, seen) + " => " + fmt(val, seen)); + }); + return "Map(" + v.size + ") {" + (mparts.length ? " " + mparts.join(", ") + " " : "") + "}"; + } + if (typeof Set === "function" && v instanceof Set) { + var sparts = []; + v.forEach(function (val) { + sparts.push(fmt(val, seen)); + }); + return "Set(" + v.size + ") {" + (sparts.length ? " " + sparts.join(", ") + " " : "") + "}"; + } + if (typeof v.BYTES_PER_ELEMENT === "number" && typeof v.length === "number") { + var tname = constructorName(v) || "TypedArray"; + var tparts = []; + for (var i = 0; i < v.length; i++) tparts.push(fmt(v[i], seen)); + return tname + "(" + v.length + ")" + (tparts.length ? " [ " + tparts.join(", ") + " ]" : " []"); + } + if (v instanceof Number) return "[Number: " + numberToString(v.valueOf()) + "]"; + if (v instanceof String) return "[String: " + quoteString(v.valueOf()) + "]"; + if (v instanceof Boolean) return "[Boolean: " + String(v.valueOf()) + "]"; + return fmtObject(v, seen); + } + + function fmtTop(v) { + if (typeof v === "string") return v; + return fmt(v, []); + } + + function makeWriter(target) { + return function () { + var parts = []; + for (var i = 0; i < arguments.length; i++) parts.push(fmtTop(arguments[i])); + target(parts.join(" ")); + }; + } + + console.log = makeWriter(function (s) { + origLog(s); + }); + console.warn = makeWriter(function (s) { + origError(s); + }); + console.error = makeWriter(function (s) { + origError(s); + }); +})(); diff --git a/test/harness-run.js b/test/harness-run.js new file mode 100644 index 00000000..0de3fc3d --- /dev/null +++ b/test/harness-run.js @@ -0,0 +1,9 @@ +// node-side driver for expected-output generation (runtime-P3). +// Usage: node harness-run.js +// Installs the harness console shim, then runs the test — the exact +// mirror of the import wrapper tester.ts compiles on the ejs side. +// Import-syntax tests (`// generator: esm`) run through this too: the +// tester tsc-transpiles them into a scratch dir (a copy of this file +// and the shim ride along) — see generateExpectedEsm in tester.ts. +require("./harness-console-shim.js"); +require(require("path").resolve(process.argv[2])); diff --git a/test/map-subclassing1.js b/test/map-subclassing1.js index ec4f5ba7..5c5a77ea 100644 --- a/test/map-subclassing1.js +++ b/test/map-subclassing1.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm // "Map is subclassable" from kangax function test() { var key = {}; diff --git a/test/map2.js b/test/map2.js index 995d81eb..204580ba 100644 --- a/test/map2.js +++ b/test/map2.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm var m = new Map(); m.set("__proto__", 5); diff --git a/test/map3.js b/test/map3.js index 64c9a73b..f13c921d 100644 --- a/test/map3.js +++ b/test/map3.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm var arr = [ ["usa", "hamburger"], diff --git a/test/map4.js b/test/map4.js index 95901add..45e5dada 100644 --- a/test/map4.js +++ b/test/map4.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm var m = new Map(); m.set("one", "uno"); diff --git a/test/map5.js b/test/map5.js index 375b740a..11684720 100644 --- a/test/map5.js +++ b/test/map5.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm var map = new Map(); map.set(+0, "foo"); diff --git a/test/map6.js b/test/map6.js new file mode 100644 index 00000000..1fbf574f --- /dev/null +++ b/test/map6.js @@ -0,0 +1,31 @@ +// Map.prototype.delete: was an unimplemented runtime stub (returned +// false, removed nothing) until the optimizer's slot-load CSE became +// its first compiler-side caller. Pins removal, size, has, get, +// iteration skipping, the return value, and re-adding after delete. + +var m = new Map(); +m.set("a", 1); +m.set("b", 2); +m.set("c", 3); + +console.log(m.delete("b")); +console.log(m.delete("nope")); +console.log(m.size); +console.log(m.has("b")); +console.log(m.get("b")); + +var keys = []; +m.forEach(function (v, k) { + keys.push(k + "=" + v); +}); +console.log(keys.join(",")); + +m.set("b", 9); +console.log(m.size); +console.log(m.get("b")); + +var it = m.keys(); +var r; +var order = []; +while (!(r = it.next()).done) order.push(r.value); +console.log(order.join(",")); diff --git a/test/math1.js b/test/math1.js index c32cb398..508a3d91 100644 --- a/test/math1.js +++ b/test/math1.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm // xfail: XXX // new ES6 math functions diff --git a/test/math2.js b/test/math2.js index 0b3d30f2..56442ba4 100644 --- a/test/math2.js +++ b/test/math2.js @@ -1,3 +1 @@ -// xfail: XXX - console.log(-0 === 0); diff --git a/test/modernization/README.md b/test/modernization/README.md new file mode 100644 index 00000000..339c2568 --- /dev/null +++ b/test/modernization/README.md @@ -0,0 +1,55 @@ +# JS Modernization census + +Probe files for the modernization effort (see `docs/plans.md`). These +are deliberately OUTSIDE the tester's `*.js` discovery glob — +most don't compile yet. Each file is a single modern-JS feature; run +one with the node-hosted compiler and diff against `node `. + +Census as of 2026-07-10 (post legacy-pipeline deletion): + +## Parser gaps (the esprima fork is the long pole) — 13 + +| probe | feature | +|---|---| +| f01 | optional chaining `?.` | +| f02 | nullish coalescing `??` | +| f03 | class fields (instance + static) | +| f04 | private fields `#x` | +| f05 | `async`/`await` | +| f06 | exponentiation `**` | +| f07 | object spread `{...a}` | +| f08 | object rest `{x, ...rest}` | +| f09 | async generators | +| f10 | `for await` | +| f11 | BigInt literals `10n` | +| f22 | trailing comma in function params | +| f23 | optional catch binding `catch {}` | +| f31 | logical assignment `??=` `\|\|=` `&&=` | +| f34 | `async` object methods (was a silent miscompile; now a loud parse error) | + +## Runtime/stdlib gaps — 4 + +| probe | missing | +|---|---| +| f12 | `String.prototype.padStart` / `replaceAll` / `at` | +| f13 | `Array.prototype.flat` / `includes` / `at` / `findLast` | +| f14 | `Object.entries` / `values` / `fromEntries` | +| f32 | `globalThis` | + +## Behavioral divergences — ALL FIXED (2026-07-10) + +| probe | divergence | fix | +|---|---|---| +| f20 | `__proto__:` literal didn't set the prototype | lowered as SetPrototypeOf (`_ejs_object_literal_set_proto`); suite test object19.js | +| f24 | regex `i`/`m` flags parsed but never passed to PCRE | `PCRE_CASELESS`/`PCRE_MULTILINE` wired through (and the compiler no longer drops `y`/`u`); suite test regexp-flags1.js | +| f26 | `generator.return()` unimplemented (and `return x` in a generator body lost its value; next/throw on a completed generator resumed a dead context) | return-sentinel unwind through the body (finally runs), completed-state tracking; suite test generator22.js | +| f34 | `async m() {}` parsed and silently miscompiled | root cause was `tolerant: true` parsing — partial ASTs from ANY syntax error were silently compiled; tolerant mode removed, parse errors are loud now (this moves f34 to the parser-gap column) | + +## Already working (17) + +Symbol.iterator generators, shorthand props, computed methods, tagged +template `.raw`, labeled blocks, `__proto__`-adjacent getters, +`new.target`, regex `s` flag parse, destructured/defaulted params, +Map/Set, Promise (then-chains), Proxy (get), class getters/static +getters, array-destructuring swap, computed accessor keys +(`{ get [k]() {} }`, as of the same day this census was taken). diff --git a/test/modernization/f01-optional-chaining.js b/test/modernization/f01-optional-chaining.js new file mode 100644 index 00000000..7b094005 --- /dev/null +++ b/test/modernization/f01-optional-chaining.js @@ -0,0 +1 @@ +let o = {a: {b: 1}}; console.log(o?.a?.b, o?.x?.y); diff --git a/test/modernization/f02-nullish.js b/test/modernization/f02-nullish.js new file mode 100644 index 00000000..c57764bc --- /dev/null +++ b/test/modernization/f02-nullish.js @@ -0,0 +1 @@ +let x = null; console.log(x ?? "dflt"); diff --git a/test/modernization/f03-class-fields.js b/test/modernization/f03-class-fields.js new file mode 100644 index 00000000..c37c900f --- /dev/null +++ b/test/modernization/f03-class-fields.js @@ -0,0 +1 @@ +class A { x = 1; static y = 2; } console.log(new A().x, A.y); diff --git a/test/modernization/f04-private-fields.js b/test/modernization/f04-private-fields.js new file mode 100644 index 00000000..04ce7bb5 --- /dev/null +++ b/test/modernization/f04-private-fields.js @@ -0,0 +1 @@ +class A { #x = 1; get() { return this.#x; } } console.log(new A().get()); diff --git a/test/modernization/f05-async-await.js b/test/modernization/f05-async-await.js new file mode 100644 index 00000000..655d24dc --- /dev/null +++ b/test/modernization/f05-async-await.js @@ -0,0 +1 @@ +async function f() { return 1; } f().then((v) => console.log(v)); diff --git a/test/modernization/f06-exponent.js b/test/modernization/f06-exponent.js new file mode 100644 index 00000000..a34cd308 --- /dev/null +++ b/test/modernization/f06-exponent.js @@ -0,0 +1 @@ +console.log(2 ** 10); diff --git a/test/modernization/f07-object-spread.js b/test/modernization/f07-object-spread.js new file mode 100644 index 00000000..b715fdfe --- /dev/null +++ b/test/modernization/f07-object-spread.js @@ -0,0 +1 @@ +let a = {x: 1}; let b = {...a, y: 2}; console.log(b.x + b.y); diff --git a/test/modernization/f08-object-rest.js b/test/modernization/f08-object-rest.js new file mode 100644 index 00000000..a21fc55e --- /dev/null +++ b/test/modernization/f08-object-rest.js @@ -0,0 +1 @@ +let {x, ...rest} = {x: 1, y: 2, z: 3}; console.log(x, rest.y + rest.z); diff --git a/test/modernization/f09-async-gen.js b/test/modernization/f09-async-gen.js new file mode 100644 index 00000000..b7a173c7 --- /dev/null +++ b/test/modernization/f09-async-gen.js @@ -0,0 +1 @@ +async function* g() { yield 1; } g().next().then((r) => console.log(r.value)); diff --git a/test/modernization/f10-for-await.js b/test/modernization/f10-for-await.js new file mode 100644 index 00000000..28378664 --- /dev/null +++ b/test/modernization/f10-for-await.js @@ -0,0 +1 @@ +async function f() { for await (let x of [1]) console.log(x); } f(); diff --git a/test/modernization/f11-bigint.js b/test/modernization/f11-bigint.js new file mode 100644 index 00000000..c0fb865a --- /dev/null +++ b/test/modernization/f11-bigint.js @@ -0,0 +1 @@ +console.log(10n + 32n); diff --git a/test/modernization/f12-string-methods.js b/test/modernization/f12-string-methods.js new file mode 100644 index 00000000..ddcb852e --- /dev/null +++ b/test/modernization/f12-string-methods.js @@ -0,0 +1 @@ +console.log("abc".padStart(5, "-"), "aa".replaceAll("a", "b"), "xy".at(-1)); diff --git a/test/modernization/f13-array-methods.js b/test/modernization/f13-array-methods.js new file mode 100644 index 00000000..6b7148fe --- /dev/null +++ b/test/modernization/f13-array-methods.js @@ -0,0 +1 @@ +console.log([1,[2,[3]]].flat(2).join(","), [1,2,3].includes(2), [1,2,3].at(-1), [3,1,2].findLast((x) => x < 3)); diff --git a/test/modernization/f14-object-methods.js b/test/modernization/f14-object-methods.js new file mode 100644 index 00000000..30112c2e --- /dev/null +++ b/test/modernization/f14-object-methods.js @@ -0,0 +1 @@ +console.log(Object.entries({a:1}).length, Object.values({a:2})[0], Object.fromEntries([["k",1]]).k); diff --git a/test/modernization/f15-symbol-iterator.js b/test/modernization/f15-symbol-iterator.js new file mode 100644 index 00000000..230aca5c --- /dev/null +++ b/test/modernization/f15-symbol-iterator.js @@ -0,0 +1 @@ +let o = { *[Symbol.iterator]() { yield 1; yield 2; } }; console.log([...o].join(",")); diff --git a/test/modernization/f16-getter-shorthand.js b/test/modernization/f16-getter-shorthand.js new file mode 100644 index 00000000..ece5a2c6 --- /dev/null +++ b/test/modernization/f16-getter-shorthand.js @@ -0,0 +1 @@ +let n = 1; let o = {n}; console.log(o.n); diff --git a/test/modernization/f17-computed-methods.js b/test/modernization/f17-computed-methods.js new file mode 100644 index 00000000..63baa220 --- /dev/null +++ b/test/modernization/f17-computed-methods.js @@ -0,0 +1 @@ +let k = "m"; let o = { [k]() { return 7; } }; console.log(o.m()); diff --git a/test/modernization/f18-tagged-raw.js b/test/modernization/f18-tagged-raw.js new file mode 100644 index 00000000..982fdad6 --- /dev/null +++ b/test/modernization/f18-tagged-raw.js @@ -0,0 +1 @@ +function t(s) { return s.raw[0]; } console.log(t`a\\nb`.length); diff --git a/test/modernization/f19-labeled-block.js b/test/modernization/f19-labeled-block.js new file mode 100644 index 00000000..ac87c667 --- /dev/null +++ b/test/modernization/f19-labeled-block.js @@ -0,0 +1 @@ +outer: { console.log("in"); break outer; console.log("no"); } console.log("out"); diff --git a/test/modernization/f20-getter-setter-proto.js b/test/modernization/f20-getter-setter-proto.js new file mode 100644 index 00000000..36700f74 --- /dev/null +++ b/test/modernization/f20-getter-setter-proto.js @@ -0,0 +1 @@ +let o = {get x() { return 1; }, __proto__: {z: 9}}; console.log(o.x, o.z); diff --git a/test/modernization/f21-new-target.js b/test/modernization/f21-new-target.js new file mode 100644 index 00000000..5e2e8cfd --- /dev/null +++ b/test/modernization/f21-new-target.js @@ -0,0 +1 @@ +function F() { console.log(new.target === F); } new F(); F(); diff --git a/test/modernization/f22-trailing-comma-fn.js b/test/modernization/f22-trailing-comma-fn.js new file mode 100644 index 00000000..4fe48f67 --- /dev/null +++ b/test/modernization/f22-trailing-comma-fn.js @@ -0,0 +1 @@ +function f(a, b,) { return a + b; } console.log(f(1, 2,)); diff --git a/test/modernization/f23-catch-no-param.js b/test/modernization/f23-catch-no-param.js new file mode 100644 index 00000000..acdb72fc --- /dev/null +++ b/test/modernization/f23-catch-no-param.js @@ -0,0 +1 @@ +try { throw 1; } catch { console.log("caught"); } diff --git a/test/modernization/f24-regex-flags.js b/test/modernization/f24-regex-flags.js new file mode 100644 index 00000000..a0bde51b --- /dev/null +++ b/test/modernization/f24-regex-flags.js @@ -0,0 +1 @@ +console.log("aAa".replace(/a/gi, "x"), /./s ? "s-ok" : ""); diff --git a/test/modernization/f25-destructure-default-fn.js b/test/modernization/f25-destructure-default-fn.js new file mode 100644 index 00000000..d333c9bc --- /dev/null +++ b/test/modernization/f25-destructure-default-fn.js @@ -0,0 +1 @@ +function f({a = 1, b = 2} = {}) { return a + b; } console.log(f(), f({a: 10})); diff --git a/test/modernization/f26-generator-return.js b/test/modernization/f26-generator-return.js new file mode 100644 index 00000000..6264fdfb --- /dev/null +++ b/test/modernization/f26-generator-return.js @@ -0,0 +1 @@ +function* g() { try { yield 1; } finally { console.log("fin"); } } let it = g(); it.next(); it.return(5); diff --git a/test/modernization/f27-map-set.js b/test/modernization/f27-map-set.js new file mode 100644 index 00000000..53856803 --- /dev/null +++ b/test/modernization/f27-map-set.js @@ -0,0 +1 @@ +let m = new Map([["a",1]]); let s = new Set([1,1,2]); console.log(m.get("a"), s.size); diff --git a/test/modernization/f28-promise.js b/test/modernization/f28-promise.js new file mode 100644 index 00000000..18d49434 --- /dev/null +++ b/test/modernization/f28-promise.js @@ -0,0 +1 @@ +Promise.resolve(42).then((v) => console.log(v)); diff --git a/test/modernization/f29-proxy.js b/test/modernization/f29-proxy.js new file mode 100644 index 00000000..e307a6f0 --- /dev/null +++ b/test/modernization/f29-proxy.js @@ -0,0 +1 @@ +let p = new Proxy({}, {get: () => 7}); console.log(p.anything); diff --git a/test/modernization/f30-getters-on-class.js b/test/modernization/f30-getters-on-class.js new file mode 100644 index 00000000..151e5a29 --- /dev/null +++ b/test/modernization/f30-getters-on-class.js @@ -0,0 +1 @@ +class A { get x() { return 1; } static get y() { return 2; } } console.log(new A().x, A.y); diff --git a/test/modernization/f31-logical-assign.js b/test/modernization/f31-logical-assign.js new file mode 100644 index 00000000..59a4c1c8 --- /dev/null +++ b/test/modernization/f31-logical-assign.js @@ -0,0 +1 @@ +let a = null; a ??= 5; let b = 0; b ||= 6; let c = 1; c &&= 7; console.log(a, b, c); diff --git a/test/modernization/f32-globalthis.js b/test/modernization/f32-globalthis.js new file mode 100644 index 00000000..45d474a2 --- /dev/null +++ b/test/modernization/f32-globalthis.js @@ -0,0 +1 @@ +console.log(typeof globalThis); diff --git a/test/modernization/f33-array-destructure-swap.js b/test/modernization/f33-array-destructure-swap.js new file mode 100644 index 00000000..2a1a4219 --- /dev/null +++ b/test/modernization/f33-array-destructure-swap.js @@ -0,0 +1 @@ +let a = 1, b = 2; [a, b] = [b, a]; console.log(a, b); diff --git a/test/modernization/f34-shorthand-async-method.js b/test/modernization/f34-shorthand-async-method.js new file mode 100644 index 00000000..93667f47 --- /dev/null +++ b/test/modernization/f34-shorthand-async-method.js @@ -0,0 +1 @@ +let o = { async m() { return 3; } }; o.m().then((v) => console.log(v)); diff --git a/test/modules1.js b/test/modules1.js index 6e573437..441eecb8 100644 --- a/test/modules1.js +++ b/test/modules1.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm import { methodInFoo1 } from "./modules1/foo1"; import { methodInFoo2 as mfoo2 } from "./modules1/foo2"; diff --git a/test/modules3.js b/test/modules3.js index 4852688c..5539dc1a 100644 --- a/test/modules3.js +++ b/test/modules3.js @@ -1,3 +1,3 @@ -// generator: babel-node +// generator: esm import "./modules1/foo1"; diff --git a/test/modules4.js b/test/modules4.js index 364238db..0785569f 100644 --- a/test/modules4.js +++ b/test/modules4.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm import * as foo4 from "./modules1/foo4"; import defaultFoo4 from "./modules1/foo4"; diff --git a/test/modules5.js b/test/modules5.js index ba9c05db..1360ba59 100644 --- a/test/modules5.js +++ b/test/modules5.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm import { method2 } from "./modules1/foo5"; method2(); diff --git a/test/modules6.js b/test/modules6.js index 9ba41b15..07a9503f 100644 --- a/test/modules6.js +++ b/test/modules6.js @@ -1,3 +1,3 @@ -// generator: babel-node +// generator: esm import "./modules6-dep"; diff --git a/test/number1.js b/test/number1.js index 46795688..a3b3e6d3 100644 --- a/test/number1.js +++ b/test/number1.js @@ -1,4 +1,3 @@ -// xfail: node outputs {} for console.log(new Number(5)), while SM and JSC output '5'. we err on the SM/JSC side of things here. console.log(Number(5)); console.log(new Number(5)); console.log(new Number(5).valueOf()); diff --git a/test/object-assign1.js b/test/object-assign1.js index b743c6e6..f0300fd3 100644 --- a/test/object-assign1.js +++ b/test/object-assign1.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm let from1 = Object.create(null); from1.prop = 5; diff --git a/test/object-setPrototypeOf1.js b/test/object-setPrototypeOf1.js index 37dbb371..e0e84d56 100644 --- a/test/object-setPrototypeOf1.js +++ b/test/object-setPrototypeOf1.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm var myproto = {}; var obj = Object.create(null); diff --git a/test/object16.js b/test/object16.js index 6c5a63e6..082e91ac 100644 --- a/test/object16.js +++ b/test/object16.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm let i = 0; let foo = { diff --git a/test/object17.js b/test/object17.js index e1f554c4..5304dd0b 100644 --- a/test/object17.js +++ b/test/object17.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm // "computed shorthand methods" from kangax function test() { var x = "y"; diff --git a/test/object18.js b/test/object18.js index b46d6132..f88164c2 100644 --- a/test/object18.js +++ b/test/object18.js @@ -1,5 +1,4 @@ -// generator: babel-node -// xfail: XXX +// generator: esm function test() { var x = "y", diff --git a/test/object19.js b/test/object19.js new file mode 100644 index 00000000..a274e2ac --- /dev/null +++ b/test/object19.js @@ -0,0 +1,15 @@ +// `__proto__:` in an object literal is a prototype definition, not an +// own property (PropertyDefinitionEvaluation / B.3.1) + +let o = { get x() { return 1; }, __proto__: { z: 9 } }; +console.log(o.x, o.z); + +let q = { __proto__: null, a: 1 }; +console.log(q.a, typeof q.toString); + +// non-object values are silently ignored +let r = { __proto__: 42, b: 2 }; +console.log(r.b, typeof r.toString); + +let s = { "__proto__": { w: 3 }, c: 4 }; +console.log(s.c, s.w); diff --git a/test/promise1.js b/test/promise1.js index 383845a2..787d4622 100644 --- a/test/promise1.js +++ b/test/promise1.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm // skip-if: runloop_impl == 'noop' Promise.resolve(5).then((value) => { diff --git a/test/promise2.js b/test/promise2.js index 3d5bdbe4..c4b68f82 100644 --- a/test/promise2.js +++ b/test/promise2.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm // skip-if: runloop_impl == 'noop' Promise.resolve(5) diff --git a/test/proxy6.js b/test/proxy6.js index 6a80a632..5af17f91 100644 --- a/test/proxy6.js +++ b/test/proxy6.js @@ -1,4 +1,3 @@ -// generator: none // from MDN let products = new Proxy( diff --git a/test/reexport1-lib.js b/test/reexport1-lib.js new file mode 100644 index 00000000..184a641b --- /dev/null +++ b/test/reexport1-lib.js @@ -0,0 +1,2 @@ +export function shout(s) { return s + "!"; } +export const LEVEL = 3; diff --git a/test/reexport1.js b/test/reexport1.js new file mode 100644 index 00000000..2c720afe --- /dev/null +++ b/test/reexport1.js @@ -0,0 +1,5 @@ +// generator: none +import { shout, LEVEL, twice } from "./reexport2-mid"; +console.log(shout("hi")); +console.log(LEVEL); +console.log(twice("yo")); diff --git a/test/reexport2-mid.js b/test/reexport2-mid.js new file mode 100644 index 00000000..ba4544f8 --- /dev/null +++ b/test/reexport2-mid.js @@ -0,0 +1,3 @@ +import { shout, LEVEL } from "./reexport1-lib"; +export { shout, LEVEL }; +export function twice(s) { return shout(shout(s)); } diff --git a/test/reflect-get1.js b/test/reflect-get1.js index 46d9e33a..4c21e5f7 100644 --- a/test/reflect-get1.js +++ b/test/reflect-get1.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm var o = { a: 5 }; var fooReceiver = { foo: 10 }; diff --git a/test/reflect-isExtensible1.js b/test/reflect-isExtensible1.js index b39ee5e6..8451ef8f 100644 --- a/test/reflect-isExtensible1.js +++ b/test/reflect-isExtensible1.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm function test(l) { try { diff --git a/test/reflect-set1.js b/test/reflect-set1.js index 124be670..8362746e 100644 --- a/test/reflect-set1.js +++ b/test/reflect-set1.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm var o = {}; var fooReceiver = { foo: "hello" }; diff --git a/test/reflect-setPrototypeOf1.js b/test/reflect-setPrototypeOf1.js index 367090ba..cf02be7b 100644 --- a/test/reflect-setPrototypeOf1.js +++ b/test/reflect-setPrototypeOf1.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm function test(l) { try { diff --git a/test/reflect1.js b/test/reflect1.js index 056d3cb9..51bb8568 100644 --- a/test/reflect1.js +++ b/test/reflect1.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm function test() { var i, diff --git a/test/regexp-flags1.js b/test/regexp-flags1.js new file mode 100644 index 00000000..fba6902d --- /dev/null +++ b/test/regexp-flags1.js @@ -0,0 +1,8 @@ +// regex flags must reach the matcher: ignoreCase and multiline were +// parsed into the RegExp object but never passed to PCRE + +console.log("aAa".replace(/a/gi, "x")); +console.log(/HeLLo/i.test("hello")); +console.log("a\nb".replace(/^b/m, "B")); +console.log("AbC".match(/[a-z]+/i)[0]); +console.log(/x/i.flags ? /x/gi.ignoreCase : "no-flags"); diff --git a/test/set1.js b/test/set1.js index 22d44e87..fa0ff080 100644 --- a/test/set1.js +++ b/test/set1.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm var mySet = new Set(); diff --git a/test/set2.js b/test/set2.js index f1e180a5..3ed371d3 100644 --- a/test/set2.js +++ b/test/set2.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm var arr = [1, 4, 6, "hallo", "there"]; var s = new Set(arr); diff --git a/test/set3.js b/test/set3.js index 9c25ae49..d363fe08 100644 --- a/test/set3.js +++ b/test/set3.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm var s = new Set(); s.add("lasagna"); diff --git a/test/set4.js b/test/set4.js index 8e28377d..e9a19d18 100644 --- a/test/set4.js +++ b/test/set4.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm var s = new Set(); s.add("lasagna"); diff --git a/test/set5.js b/test/set5.js index 838ba950..b435c8c7 100644 --- a/test/set5.js +++ b/test/set5.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm var set = new Set(); set.add(+0); diff --git a/test/shapes-storm1.js b/test/shapes-storm1.js new file mode 100644 index 00000000..25179d7c --- /dev/null +++ b/test/shapes-storm1.js @@ -0,0 +1,153 @@ +// shapes-plan P4.2 stress: transition churn across every shaped-mode +// boundary — adds, repr flips, deletes, attribute/accessor migration, +// symbol and index keys, freeze/seal, enumeration order, `in` checks. + +// plain construction + repr flips +var objs = []; +for (var i = 0; i < 200; i++) { + var o = { a: i, b: "s" + i }; + o.c = i * 1.5; + o.d = i % 2 === 0 ? i : "odd" + i; // alternating repr chains + o.c = "now-a-string-" + i; // repr flip after the fact + objs.push(o); +} +var sum = 0; +for (var i = 0; i < 200; i++) { + sum += objs[i].a; + sum += objs[i].c.length; +} +console.log("sum", sum); + +// delete drops to dictionary; re-add after delete +var del = { x: 1, y: 2, z: 3 }; +delete del.y; +console.log("del keys", Object.keys(del).join(",")); +del.y = 42; +del.w = 5; +console.log("del keys2", Object.keys(del).join(","), del.y, "y" in del, "v" in del); + +// non-default attributes migrate +var attr = { p: 1, q: 2 }; +Object.defineProperty(attr, "r", { value: 3, enumerable: false }); +console.log("attr keys", Object.keys(attr).join(","), attr.r); +// (getOwnPropertyNames on all-enumerable objects only: echojs has a +// pre-existing, mode-independent bug that filters non-enumerable names) +console.log("attr names", Object.getOwnPropertyNames({ p: 1, q: 2 }).join(",")); + +// plain defineProperty with default attrs stays shaped +var dp = {}; +Object.defineProperty(dp, "k", { value: 7, writable: true, enumerable: true, configurable: true }); +dp.m = 8; +console.log("dp", dp.k, dp.m, Object.keys(dp).join(",")); + +// accessors migrate +var acc = { base: 10 }; +Object.defineProperty(acc, "twice", { + get: function () { return this.base * 2; }, + enumerable: true, + configurable: true, +}); +acc.base = 21; +console.log("acc", acc.twice, Object.keys(acc).join(",")); + +// getOwnPropertyDescriptor on a shaped object +var god = { s: "str", n: 4.25 }; +var d = Object.getOwnPropertyDescriptor(god, "n"); +console.log("desc", d.value, d.writable, d.enumerable, d.configurable, d.get === undefined); + +// index-looking keys migrate +var idx = { name: "x" }; +idx["0"] = "zero"; +idx.after = true; +console.log("idx", idx[0], idx.name, idx.after, Object.keys(idx).join(",")); + +// freeze/seal +var froz = { f: 1, g: 2 }; +Object.freeze(froz); +froz.f = 99; +froz.h = 3; +console.log("froz", froz.f, froz.h, Object.isFrozen(froz), Object.isExtensible(froz)); +var seal = { f: 1 }; +Object.seal(seal); +seal.f = 2; +delete seal.f; +console.log("seal", seal.f, Object.isSealed(seal)); + +// preventExtensions keeps existing fields writable +var pe = { a: 1 }; +Object.preventExtensions(pe); +pe.a = 2; +pe.b = 3; +console.log("pe", pe.a, pe.b, Object.isExtensible(pe)); + +// for-in order, proto chain +var proto = { inherited: "p" }; +var child = Object.create(proto); +child.own1 = 1; +child.own2 = 2; +var forin = []; +for (var k in child) forin.push(k); +console.log("forin", forin.join(",")); +console.log("hasOwn", child.hasOwnProperty("own1"), child.hasOwnProperty("inherited"), "inherited" in child); + +// Object.assign shaped -> shaped and shaped -> dict +var tgt = { t: 0 }; +var src = { u: 1, v: "two" }; +Object.assign(tgt, src); +console.log("assign", JSON.stringify(tgt)); +var dictTgt = { q: 1 }; +delete dictTgt.q; // dict mode now +Object.assign(dictTgt, { r: 2, s: 3 }); +console.log("assign2", JSON.stringify(dictTgt)); + +// defineProperties driven by a shaped descriptor object +var dst = {}; +Object.defineProperties(dst, { + one: { value: 1, enumerable: true, writable: true, configurable: true }, + two: { value: 2, enumerable: true }, +}); +console.log("defprops", dst.one, dst.two, Object.keys(dst).join(",")); + +// symbol keys migrate but stay invisible to string enumeration +var sym = Symbol("secret"); +var symObj = { visible: 1 }; +symObj[sym] = "hidden"; +symObj.visible2 = 2; +console.log("sym", symObj[sym], Object.keys(symObj).join(","), Object.getOwnPropertySymbols(symObj).length); + +// wide object crossing the slot-growth boundaries (4/8/16/32) +var wide = {}; +for (var i = 0; i < 40; i++) wide["f" + i] = i; +var wsum = 0; +for (var i = 0; i < 40; i++) wsum += wide["f" + i]; +console.log("wide", wsum, Object.keys(wide).length, wide.f0, wide.f39); + +// long-lived churn: many transitions on one object graph +var churn = {}; +for (var i = 0; i < 60; i++) { + churn["k" + i] = i; + if (i % 7 === 0) churn["k" + i] = "flip" + i; +} +console.log("churn", Object.keys(churn).length, churn.k0, churn.k7, churn.k59); + +// JSON round-trip of shaped objects +var jr = JSON.parse('{"a":1,"b":[1,2,3],"c":{"d":"e"}}'); +jr.f = jr.a + jr.b[2]; +console.log("json", JSON.stringify(jr)); + +// spread/rest-free duplicate-literal shapes share transitions +function mk(x, y) { return { x: x, y: y }; } +var pts = []; +for (var i = 0; i < 100; i++) pts.push(mk(i, i * 2)); +var psum = 0; +for (var i = 0; i < 100; i++) psum += pts[i].x + pts[i].y; +console.log("pts", psum); + +// value update through Object.defineProperty on an existing shaped field +var upd = { z: 1 }; +Object.defineProperty(upd, "z", { value: "replaced" }); +console.log("upd", upd.z, Object.keys(upd).join(",")); + +// toString / propertyIsEnumerable / valueOf via proto on shaped receivers +var pie = { e: 1 }; +console.log("pie", pie.propertyIsEnumerable("e"), pie.propertyIsEnumerable("nope"), Object.prototype.toString.call(pie)); diff --git a/test/shiftassign1.js b/test/shiftassign1.js new file mode 100644 index 00000000..694f1dac --- /dev/null +++ b/test/shiftassign1.js @@ -0,0 +1,9 @@ +function t() { + let a = 12; a |= 1; console.log(a); + let b = 13; b ^= 2; console.log(b); + let c = 15; c >>= 1; console.log(c); + let d = 5; d <<= 2; console.log(d); + let e = 20; e >>>= 2; console.log(e); + let f = 7; f &= 5; console.log(f); +} +t(); diff --git a/test/shorthand-method1.js b/test/shorthand-method1.js index c5ca48e0..73d9c908 100644 --- a/test/shorthand-method1.js +++ b/test/shorthand-method1.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm console.log( { diff --git a/test/shorthand-prop1.js b/test/shorthand-prop1.js index e6d79216..8ba58de9 100644 --- a/test/shorthand-prop1.js +++ b/test/shorthand-prop1.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm var hello = "world"; var obj = { hello }; diff --git a/test/slice-negative1.js b/test/slice-negative1.js new file mode 100644 index 00000000..f12f4902 --- /dev/null +++ b/test/slice-negative1.js @@ -0,0 +1,7 @@ +let a = [1, 2, 3, 4, 5]; +console.log(a.slice(0, -1).join(",")); +console.log(a.slice(-2).join(",")); +console.log(a.slice(-4, -1).join(",")); +console.log(a.slice(1, -10).length); +console.log(a.slice(0, undefined).join(",")); +console.log(a.slice(-100).join(",")); diff --git a/test/sparsearray1.js b/test/sparsearray1.js index 4eeebdbd..21302e46 100644 --- a/test/sparsearray1.js +++ b/test/sparsearray1.js @@ -1,4 +1,3 @@ -// xfail: sparse array support is pretty weak and full of NOT_IMPLEMENTED's var arr = new Array(1000000000); arr[0] = "Hello World"; console.log(arr[0]); diff --git a/test/spread1.js b/test/spread1.js index b2acff3d..309b1711 100644 --- a/test/spread1.js +++ b/test/spread1.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm function foo(...args) { console.log(args.length); diff --git a/test/spread2.js b/test/spread2.js index efb4912e..82422be4 100644 --- a/test/spread2.js +++ b/test/spread2.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm // this file should fail with a syntax error due to the arguments usage function foo(...args) { diff --git a/test/spread3.js b/test/spread3.js index 6e069a7b..f56e6381 100644 --- a/test/spread3.js +++ b/test/spread3.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm function foo(a, b, c) { console.log(a); diff --git a/test/spread4.js b/test/spread4.js index 57be2a6d..f3752761 100644 --- a/test/spread4.js +++ b/test/spread4.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm // testing closing over rest parameters diff --git a/test/spread5.js b/test/spread5.js index ac95a84c..48239e52 100644 --- a/test/spread5.js +++ b/test/spread5.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm console.log([1, ...[2, 3, 4], 5][3]); console.log([...[1, 2, 3]][2]); diff --git a/test/spread6.js b/test/spread6.js index dbdf714f..0ecadd5f 100644 --- a/test/spread6.js +++ b/test/spread6.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm function Foo() {} Foo.prototype[Symbol.iterator] = function () { diff --git a/test/spread7.js b/test/spread7.js index fdb3955e..019f5225 100644 --- a/test/spread7.js +++ b/test/spread7.js @@ -1,3 +1,3 @@ -// generator: babel-node +// generator: esm for (var x of [...[1, 2, 3, 4, 5, 8, 7]]) console.log(x); diff --git a/test/string-codePointAt1.js b/test/string-codePointAt1.js index 96ccc817..de7c1dab 100644 --- a/test/string-codePointAt1.js +++ b/test/string-codePointAt1.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm console.log("ABC".codePointAt(1)); // 66 console.log("\uD800\uDC00".codePointAt(0)); // 65536 diff --git a/test/string-contains1.js b/test/string-contains1.js index 4fca0107..7f3b9666 100644 --- a/test/string-contains1.js +++ b/test/string-contains1.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm var str = "To be, or not to be, that is the question."; diff --git a/test/string-endsWith1.js b/test/string-endsWith1.js index 53edc2d3..5689c51b 100644 --- a/test/string-endsWith1.js +++ b/test/string-endsWith1.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm var str = "To be, or not to be, that is the question."; diff --git a/test/string-iter1.js b/test/string-iter1.js index 61abfe77..786677dd 100644 --- a/test/string-iter1.js +++ b/test/string-iter1.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm var s = "hallo dudes"; diff --git a/test/string-raw1.js b/test/string-raw1.js index f925594d..de70ea3e 100644 --- a/test/string-raw1.js +++ b/test/string-raw1.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm let world = "world"; diff --git a/test/string-repeat1.js b/test/string-repeat1.js index dabe6849..ebadfb80 100644 --- a/test/string-repeat1.js +++ b/test/string-repeat1.js @@ -1,4 +1,4 @@ -//generator: babel-node +//generator: esm try { console.log("abc".repeat(-1)); // RangeError diff --git a/test/string-startsWith1.js b/test/string-startsWith1.js index ead27730..a2657da4 100644 --- a/test/string-startsWith1.js +++ b/test/string-startsWith1.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm var str = "To be, or not to be, that is the question."; diff --git a/test/symbol-iterator1.js b/test/symbol-iterator1.js index b7e968e6..66c2e365 100644 --- a/test/symbol-iterator1.js +++ b/test/symbol-iterator1.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm function test() { var a = 0, diff --git a/test/symbol-tostringtag1.js b/test/symbol-tostringtag1.js index 0e66487e..79a2352f 100644 --- a/test/symbol-tostringtag1.js +++ b/test/symbol-tostringtag1.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm var a = {}; a[Symbol.toStringTag] = "foo"; diff --git a/test/symbol-tostringtag2.js b/test/symbol-tostringtag2.js index cb0f65f7..a150fb93 100644 --- a/test/symbol-tostringtag2.js +++ b/test/symbol-tostringtag2.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm class Foo { get [Symbol.toStringTag]() { diff --git a/test/symbol2.js b/test/symbol2.js index b43418d8..5d972680 100644 --- a/test/symbol2.js +++ b/test/symbol2.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm // from kangax's table diff --git a/test/template-nested1.js b/test/template-nested1.js new file mode 100644 index 00000000..bc032cb2 --- /dev/null +++ b/test/template-nested1.js @@ -0,0 +1,6 @@ +// generator: none +function f(xs) { + return `(${xs.map((p) => `${p}!`).join(", ")})`; +} +console.log(f(["a", "b"])); +console.log(`x${`y${1 + 1}z`}w`); diff --git a/test/template-string1.js b/test/template-string1.js index a085fc7c..a94be1ff 100644 --- a/test/template-string1.js +++ b/test/template-string1.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm var mundo = "world"; diff --git a/test/tester-deps.d.ts b/test/tester-deps.d.ts new file mode 100644 index 00000000..73eb2deb --- /dev/null +++ b/test/tester-deps.d.ts @@ -0,0 +1,13 @@ +// Hand-written surface declarations for tester.ts's untyped deps. +// (glob and colors ship their own types; temp does not.) + +declare module "temp" { + interface OpenFileInfo { + path: string; + fd: number; + } + export function open( + affixes: string, + callback: (err: Error | null, info: OpenFileInfo) => void + ): void; +} diff --git a/test/tester.js b/test/tester.js deleted file mode 100644 index 3bd26764..00000000 --- a/test/tester.js +++ /dev/null @@ -1,402 +0,0 @@ -#!/usr/bin/env node - -const path = require("path"), - os = require("os"), - fs = require("fs"), - { globSync } = require("glob"), - child_process = require("child_process"), - spawn = child_process.spawn, - exec = child_process.exec, - colors = require("colors/safe"), - temp = require("temp"); - -// maps from test_name -> properties as defined in the test file -const skip_ifs = Object.create(null); // `// skip-if: ...` an expression, evaled. if true, ignore the test -const xfails = Object.create(null); // `// xfail: ...` test is expected to fail. ... is the reason -const generators = Object.create(null); // `// generator: ...` ... is the executable used to generate expected output - -const expected_names = Object.create(null); -const expected_stdouts = Object.create(null); -const stdouts = Object.create(null); - -const failed_tests = []; - -// index here is the stage #. 0 = run it under node, 1 = run it with stage1, 2 = run it with stage2 -const compilers = ["../ejs", "../ejs.exe.stage1", "../ejs.exe.stage2"]; - -let runloop_impl = require("../lib/generated/lib/host-config.js").RUNLOOP_IMPL; - -const running_in_ci = process.env["CIRCLE_BUILD_NUM"] != null; - -let platform_to_test = null; - -let stage_to_run = 0; - -let test_threads = 4; - -const result_types = { - fail: { str: "FAIL", colorizer: colors.red.bold }, - xfail: { str: "xfail", colorizer: colors.yellow }, - xpass: { str: "ERROR", colorizer: colors.red.bold }, - pass: { str: "pass", colorizer: colors.green }, -}; - -const fail_str = "fail"; -const xfail_str = "xfail"; -const xpass_str = "xpass"; -const pass_str = "pass"; - -function timerStart() { - return process.hrtime(); -} -// from http://stackoverflow.com/questions/10617070/how-to-measure-execution-time-of-javascript-code-with-callbacks -function getElapsed(start_time) { - let elapsed = process.hrtime(start_time); - let elapsed_ms = elapsed[0] * 1000 + elapsed[1] / 1000000; - return elapsed_ms.toFixed(2); // 2 decimal places -} - -function makeJustifierColumn(columns, leftJustify) { - let spaces = Array(columns).join(" "); - return function (str, transformer) { - let padding = spaces.substr(0, columns - str.length); - if (transformer) str = transformer(str); - if (leftJustify) return str + padding; - else return padding + str; - }; -} - -function makeNoopColumn() { - return function (x) { - return x; - }; -} - -const testColumn = makeJustifierColumn(40, false); -const resultColumn = makeJustifierColumn(5, true); // maximum length of fail/xfail/xpass/pass -const timeColumn = makeJustifierColumn(11, false); // enough to hold "XXXXX.XX ms". -const errStringColumn = makeNoopColumn(); - -function writeOutput(test_name, result_type, elapsed, err_string) { - let elapsed_str = elapsed == null ? "?" : elapsed; - - console.log( - testColumn(test_name), - resultColumn(result_types[result_type].str, result_types[result_type].colorizer), - timeColumn(elapsed_str + " ms"), - errStringColumn(err_string ? err_string : "") - ); -} - -function testFailure(test_name, err_string, elapsed, additional) { - writeOutput(test_name, fail_str, elapsed, "(" + err_string + ")"); - console.log(additional); - failed_tests.push(test_name); -} - -function testUnexpectedPass(test_name, elapsed) { - writeOutput(test_name, xpass_str, elapsed, "(unexpected pass)"); - failed_tests.push(test_name); -} - -function testFailed(test_name, err_string, elapsed, additional) { - if (xfails[test_name]) { - writeOutput(test_name, xfail_str, elapsed, "(" + xfails[test_name] + ")"); - } else { - testFailure(test_name, err_string, elapsed, additional); - } -} - -function checkStdout(test_name, elapsed, cb) { - if (stdouts[test_name] != expected_stdouts[test_name]) { - temp.open("ejstest-received", function (err, info) { - fs.writeSync(info.fd, stdouts[test_name]); - fs.close(info.fd, function (err) { - exec( - "/usr/bin/diff -u " + expected_names[test_name] + " " + info.path, - function (err, stdout) { - testFailed(test_name, "stdout doesn't match", elapsed, stdout); - cb(); - } - ); - }); - }); - } else { - if (xfails[test_name]) { - testUnexpectedPass(test_name, elapsed); - } else { - writeOutput(test_name, pass_str, elapsed); - } - - setTimeout(cb, 0); - } -} - -function shouldGenerateExpectedOutput(test_file, expected_file) { - let test_stat = fs.statSync(test_file); - try { - let expected_stat = fs.statSync(expected_file); - return test_stat.mtime.getTime() > expected_stat.mtime.getTime(); - } catch (e) { - // XXX verify that e == ENOENT - return true; - } -} - -function processOneTest(gen_expected, test, cb) { - let test_name = path.basename(test); - - //if (!gen_expected) console.log("processOneTest(" + gen_expected + ", " + test_name + ")"); - if (skip_ifs[test_name]) { - if (eval(skip_ifs[test_name])) { - //console.log("skipping " + test_name); - setTimeout(cb, 0); - return; - } - } - - if (gen_expected) { - const expected_name = "./expected/" + test_name + ".expected-out"; - - const should_generate = shouldGenerateExpectedOutput(test, expected_name); - - expected_names[test_name] = expected_name; - const generator = generators[test_name] || "node"; - if (should_generate && generator !== "none") { - console.log("generating expected output for " + test_name + " using " + generator); - - exec(generator + " " + test + " > " + expected_name, function (err, stdout) { - if (err) { - cb(err); - return; - } - expected_stdouts[test_name] = fs.readFileSync(expected_name).toString(); - cb(); - }); - } else { - try { - expected_stdouts[test_name] = fs.readFileSync(expected_name).toString(); - } catch (e) { - setTimeout(() => cb(e), 0); - return; - } - setTimeout(cb, 0); - } - return; - } else { - try { - const start = timerStart(); - const platform_target = platform_to_test ? ["--target", platform_to_test] : []; - const ccomp = spawn( - compilers[stage_to_run], - platform_target.concat([ - "--srcdir", - "--moduledir", - "../node-compat", - "--moduledir", - "../ejs-llvm", - test, - ]) - ); - ccomp.on("exit", function (code, errstring) { - if (code !== 0) { - const elapsed = getElapsed(start); - testFailed(test_name, `compiler failed (exit code = ${code})`, elapsed); - cb(); - return; - } - // XXX check code to make sure we were successful? - let env; - if (platform_to_test === "sim") { - process.env["EJS_FORCE_STDOUT"] = "1"; - process.env["DYLD_FRAMEWORK_PATH"] = - "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/System/Library/Frameworks:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/System/Library/PrivateFrameworks"; - process.env["DYLD_LIBRARY_PATH"] = - "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/usr/lib:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/usr/lib/system:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/System/Library/PrivateFrameworks/FontServices.framework:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/System/Library/Frameworks/Accelerate.framework/Frameworks/vecLib.framework:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk//System/Library/Frameworks/OpenGLES.framework"; - } - - const cexec = spawn("./" + test + ".exe"); - let test_stdout = ""; - let test_stderr = ""; - cexec.on("close", function (code, errstring) { - stdouts[test_name] = test_stdout; - - // XXX check code to make sure we were successful? - - var elapsed = getElapsed(start); - checkStdout(test_name, elapsed, cb); - }); - cexec.on("error", function (err) { - var elapsed = getElapsed(start); - testFailed(test_name, err.toString(), elapsed); - cb(); - }); - cexec.stdout.on("data", function (msg) { - test_stdout += msg; - }); - cexec.stderr.on("data", function (msg) { - test_stderr += msg; - }); - }); - ccomp.on("error", function (err) { - const elapsed = getElapsed(start); - testFailed(test_name, err.toString(), elapsed); - cb(); - return; - }); - } catch (e) { - console.log(e); - setTimeout(cb, 0); - return; - } - } -} - -function processTests(gen_expected, tests, cb) { - let i = 0; - const e = tests.length; - - let num_outstanding = 0; - - const processTestCb = function () { - //console.log("processTestCb"); - i++; - num_outstanding--; - if (i >= e) { - //console.log("doing setTimeout"); - if (num_outstanding == 0) { - setTimeout(cb, 0); - } - return; - } - - num_outstanding++; - processOneTest(gen_expected, tests[i], processTestCb); - }; - - for (let j = 0; j < test_threads; j++) { - processOneTest(gen_expected, tests[i++], processTestCb); - - num_outstanding++; - } -} - -function readTest(test) { - const test_name = path.basename(test); - const contents = fs.readFileSync(test).toString(); - const lines = contents.split("\n"); - - // read the comments at the start, and pull out useful info - for (let i = 0, e = lines.length; i < e; i++) { - let line = lines[i]; - if (line.indexOf("//") !== 0) { - return; - } - - line = line.substr(2).trim(); - - if (line.indexOf("skip-if:") === 0) { - if (skip_ifs[test_name]) - throw new Error("test " + test + " already has a skip-if: directive"); - skip_ifs[test_name] = line.substr("skip-if:".length).trim(); - } - - if (line.indexOf("xfail:") === 0) { - if (xfails[test_name]) - throw new Error("test " + test + " already has a xfail: directive"); - xfails[test_name] = line.substr("xfail:".length).trim(); - } - - if (line.indexOf("generator:") === 0) { - if (generators[test_name]) - throw new Error("test " + test + " already has a generator: directive"); - generators[test_name] = line.substr("generator:".length).trim(); - } - } -} - -const args = process.argv.slice(2); - -let test_to_run = null; - -if (args[0] == "-p") { - args.shift(); - if (args.length < 1) { - throw new Error("-p requires an argument [osx, sim]"); - } - platform_to_test = args.shift(); - if (platform_to_test !== "osx" && platform_to_test !== "sim") { - throw new Error("-p requires an argument [osx, sim]"); - } -} - -if (args[0] == "-s") { - args.shift(); - if (args.length < 1) throw new Error("-s requires an argument between 0 and 2"); - stage_to_run = parseInt(args.shift()); - if (stage_to_run < 0 && stage_to_run > 2) - throw new Error("-s requires an argument between 0 and 2"); -} -if (args[0] == "-t") { - args.shift(); - if (args.length < 1) throw new Error("-t requires an argument (the test file to run)"); - test_to_run = args.shift(); - test_threads = 1; // XXX workaround for a bug, but we also only need 1 thread when we're running 1 test -} - -function runTests(tests) { - tests.forEach(readTest); - - if (tests.length == 1) - console.log( - "running " + - tests[0] + - " against stage " + - stage_to_run + - " (" + - compilers[stage_to_run] + - ")" - ); - else - console.log( - "running " + - tests.length + - " tests against stage " + - stage_to_run + - " (" + - compilers[stage_to_run] + - ")" - ); - - processTests(true, tests, function (err) { - if (err) { - console.log(err); - process.exit(1); - } - processTests(false, tests, function () { - const run_failed = failed_tests.length > 0; - if (run_failed > 0) { - console.log(); - console.log(testColumn(failed_tests.length + " failed tests")); - console.log(testColumn("================")); - failed_tests.forEach(function (t) { - console.log(testColumn(t)); - }); - } - console.log( - testColumn(" "), - resultColumn("done", result_types[run_failed ? "fail" : "pass"].colorizer) - ); - process.exit(run_failed ? -1 : 0); - }); - }); -} - -if (test_to_run) { - runTests([test_to_run]); -} else { - // run all the tests - - let tests = globSync("./*+([0-9]).js"); - runTests(tests); -} diff --git a/test/tester.ts b/test/tester.ts new file mode 100644 index 00000000..f5389551 --- /dev/null +++ b/test/tester.ts @@ -0,0 +1,580 @@ +// The test-suite runner. Compiled to tester.js by tsc (see +// tsconfig.json in this directory); buck-test-stage.sh does that when +// it stages the test tree, so the staged copy always runs from these +// sources. + +import * as path from "path"; +import * as os from "os"; +import * as fs from "fs"; +import { globSync } from "glob"; +import * as child_process from "child_process"; +import * as colors from "colors/safe"; +import * as temp from "temp"; + +const spawn = child_process.spawn; +const exec = child_process.exec; + +type Colorizer = (s: string) => string; + +// maps from test_name -> properties as defined in the test file +const skip_ifs: Record = Object.create(null); // `// skip-if: ...` an expression, evaled. if true, ignore the test +const xfails: Record = Object.create(null); // `// xfail: ...` test is expected to fail. ... is the reason +const generators: Record = Object.create(null); // `// generator: ...` how expected output is generated (node | esm | none) + +const expected_names: Record = Object.create(null); +const expected_stdouts: Record = Object.create(null); +const stdouts: Record = Object.create(null); + +const failed_tests: string[] = []; + +// index here is the stage #. 0 = run it under node, 1 = run it with stage1, 2 = run it with stage2 +const compilers = ["../ejs", "../ejs.exe.stage1", "../ejs.exe.stage2", "../ejs.exe.stage3"]; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const runloop_impl: string = require("../lib/generated/lib/host-config.js").RUNLOOP_IMPL; +// referenced from `// skip-if:` expressions, which eval in this scope +void runloop_impl; + +// baselines must not depend on the timezone of the machine that generated +// them: local-time Date construction (date3.js) feeds the value-based +// serializer's UTC rendering, so generation and test runs both pin UTC +process.env.TZ = "UTC"; + +let platform_to_test: string | null = null; + +let stage_to_run = 0; + +let test_threads = 4; + +// colors' chained styles (red.bold) aren't in its shipped types +const red_bold = (colors.red as unknown as { bold: Colorizer }).bold; + +type ResultKind = "fail" | "xfail" | "xpass" | "pass"; + +const result_types: Record = { + fail: { str: "FAIL", colorizer: red_bold }, + xfail: { str: "xfail", colorizer: colors.yellow }, + xpass: { str: "ERROR", colorizer: red_bold }, + pass: { str: "pass", colorizer: colors.green }, +}; + +function timerStart(): [number, number] { + return process.hrtime(); +} +// from http://stackoverflow.com/questions/10617070/how-to-measure-execution-time-of-javascript-code-with-callbacks +function getElapsed(start_time: [number, number]): string { + let elapsed = process.hrtime(start_time); + let elapsed_ms = elapsed[0] * 1000 + elapsed[1] / 1000000; + return elapsed_ms.toFixed(2); // 2 decimal places +} + +function makeJustifierColumn(columns: number, leftJustify: boolean) { + let spaces = Array(columns).join(" "); + return function (str: string, transformer?: Colorizer): string { + let padding = spaces.substr(0, columns - str.length); + if (transformer) str = transformer(str); + if (leftJustify) return str + padding; + else return padding + str; + }; +} + +function makeNoopColumn() { + return function (x: string): string { + return x; + }; +} + +const testColumn = makeJustifierColumn(40, false); +const resultColumn = makeJustifierColumn(5, true); // maximum length of fail/xfail/xpass/pass +const timeColumn = makeJustifierColumn(11, false); // enough to hold "XXXXX.XX ms". +const errStringColumn = makeNoopColumn(); + +function writeOutput( + test_name: string, + result_type: ResultKind, + elapsed: string | null, + err_string?: string +): void { + let elapsed_str = elapsed == null ? "?" : elapsed; + + console.log( + testColumn(test_name), + resultColumn(result_types[result_type].str, result_types[result_type].colorizer), + timeColumn(elapsed_str + " ms"), + errStringColumn(err_string ? err_string : "") + ); +} + +function testFailure( + test_name: string, + err_string: string, + elapsed: string | null, + additional?: string +): void { + writeOutput(test_name, "fail", elapsed, "(" + err_string + ")"); + console.log(additional); + failed_tests.push(test_name); +} + +function testUnexpectedPass(test_name: string, elapsed: string | null): void { + writeOutput(test_name, "xpass", elapsed, "(unexpected pass)"); + failed_tests.push(test_name); +} + +function testFailed( + test_name: string, + err_string: string, + elapsed: string | null, + additional?: string +): void { + const xfail = xfails[test_name]; + if (xfail) { + writeOutput(test_name, "xfail", elapsed, "(" + xfail + ")"); + } else { + testFailure(test_name, err_string, elapsed, additional); + } +} + +function checkStdout(test_name: string, elapsed: string, cb: () => void): void { + if (stdouts[test_name] != expected_stdouts[test_name]) { + temp.open("ejstest-received", function (err, info) { + fs.writeSync(info.fd, stdouts[test_name] ?? ""); + fs.close(info.fd, function () { + exec( + "/usr/bin/diff -u " + expected_names[test_name] + " " + info.path, + function (err, stdout) { + testFailed(test_name, "stdout doesn't match", elapsed, stdout); + cb(); + } + ); + }); + }); + } else { + if (xfails[test_name]) { + testUnexpectedPass(test_name, elapsed); + } else { + writeOutput(test_name, "pass", elapsed); + } + + setTimeout(cb, 0); + } +} + +// the value-based harness (runtime-P3): tests generate and run with +// console.log replaced by the serializer in harness-console-shim.js, on +// both sides, so baselines assert on values, not on node's inspect format +const harness_shim = "harness-console-shim.js"; +const harness_run = "harness-run.js"; + +function shouldGenerateExpectedOutput(test_file: string, expected_file: string): boolean { + try { + let expected_mtime = fs.statSync(expected_file).mtime.getTime(); + // the harness serializer contributes to the expected output too — + // editing it must refresh every baseline + let newest = fs.statSync(test_file).mtime.getTime(); + for (const dep of [harness_shim, harness_run]) { + try { + newest = Math.max(newest, fs.statSync(dep).mtime.getTime()); + } catch (e) {} + } + return newest > expected_mtime; + } catch (e) { + // XXX verify that e == ENOENT + return true; + } +} + +// import-syntax tests (`// generator: esm`) can't run under plain node: +// their relative import specifiers are extensionless (the compiler's +// gather-imports requires import syntax, node's ESM loader requires +// extensions). tsc transpiles the test and its relative-import closure +// to CommonJS in a scratch dir (compiler-P2; babel-node's require hook +// did this until then) and node runs the transpiled copy through the +// same harness-run driver. +function relativeImportClosure(test: string): string[] { + const seen = new Set(); + const files: string[] = []; + const visit = function (file: string): void { + const resolved = path.resolve(file); + if (seen.has(resolved)) return; + seen.add(resolved); + files.push(resolved); + const src = fs.readFileSync(resolved, "utf-8"); + const import_re = /^\s*(?:import|export)\b[^;]*?["']([^"']+)["']/gm; + let m: RegExpExecArray | null; + while ((m = import_re.exec(src)) !== null) { + const spec = m[1]; + if (spec == null || spec[0] !== ".") continue; + let dep = path.join(path.dirname(resolved), spec); + if (!dep.endsWith(".js")) { + // extensionless specifiers resolve like the compiler's: + // file first, then directory/index.js (modules6) + if (fs.existsSync(dep + ".js")) dep += ".js"; + else dep = path.join(dep, "index.js"); + } + visit(dep); + } + }; + visit(test); + return files; +} + +const tsc_bin = path.join(path.dirname(require.resolve("typescript/package.json")), "bin", "tsc"); + +function generateExpectedEsm( + test: string, + expected_name: string, + cb: (err?: Error | null) => void +): void { + const gen_tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), "ejstest-esm-")); + const cleanup = function (): void { + try { + fs.rmSync(gen_tmpdir, { recursive: true, force: true }); + } catch (e) {} + }; + const closure = relativeImportClosure(test) + .map((f) => '"' + f + '"') + .join(" "); + exec( + 'node "' + + tsc_bin + + '" --ignoreConfig --allowJs --target es2016 --module commonjs' + + ' --esModuleInterop --outDir "' + + gen_tmpdir + + '" ' + + closure, + function (err) { + if (err) { + cleanup(); + cb(err); + return; + } + // the harness files are plain ES5 CommonJS — they ride along + // unconverted so generation runs the byte-exact serializer + for (const f of [harness_shim, harness_run]) { + fs.copyFileSync(f, path.join(gen_tmpdir, f)); + } + const transpiled = path.join(gen_tmpdir, path.basename(test)); + exec( + 'node "' + + path.join(gen_tmpdir, harness_run) + + '" "' + + transpiled + + '" > ' + + expected_name, + function (err) { + cleanup(); + cb(err); + } + ); + } + ); +} + +function processOneTest(gen_expected: boolean, test: string, cb: (err?: Error | null) => void): void { + let test_name = path.basename(test); + + //if (!gen_expected) console.log("processOneTest(" + gen_expected + ", " + test_name + ")"); + const skip_if = skip_ifs[test_name]; + if (skip_if) { + if (eval(skip_if)) { + //console.log("skipping " + test_name); + setTimeout(cb, 0); + return; + } + } + + if (gen_expected) { + const expected_name = "./expected/" + test_name + ".expected-out"; + + const should_generate = shouldGenerateExpectedOutput(test, expected_name); + + expected_names[test_name] = expected_name; + const generator = generators[test_name] || "node"; + if (should_generate && generator !== "none") { + console.log("generating expected output for " + test_name + " using " + generator); + + const generated = function (err?: Error | null): void { + if (err) { + cb(err); + return; + } + expected_stdouts[test_name] = fs.readFileSync(expected_name).toString(); + cb(); + }; + if (generator === "esm") { + generateExpectedEsm(test, expected_name, generated); + } else { + exec(generator + " " + harness_run + " " + test + " > " + expected_name, generated); + } + } else { + try { + expected_stdouts[test_name] = fs.readFileSync(expected_name).toString(); + } catch (e) { + setTimeout(() => cb(e as Error), 0); + return; + } + setTimeout(cb, 0); + } + return; + } else { + try { + const start = timerStart(); + const platform_target = platform_to_test ? ["--target", platform_to_test] : []; + const extra_flags = process.env.EJS_EXTRA_FLAGS + ? process.env.EJS_EXTRA_FLAGS.split(" ") + : []; + // generator:none tests keep the legacy path (raw stdout against + // a checked-in baseline, no shim); everything else compiles a + // generated wrapper that imports the console shim, then the + // test — the mirror of harness-run.js on the node side + let compile_target = test; + let output_args: string[] = []; + let wrapper_name: string | null = null; + if (generators[test_name] !== "none") { + wrapper_name = ".__wrap__." + test_name; + const spec = "./" + test_name.replace(/\.js$/, ""); + fs.writeFileSync( + wrapper_name, + "// generated by tester.ts (value-based harness); deleted after compile\n" + + 'import "./' + harness_shim.replace(/\.js$/, "") + '";\n' + + 'import "' + spec + '";\n' + ); + compile_target = "./" + wrapper_name; + output_args = ["-o", test + ".exe"]; + } + // per-test TMPDIR (the types-diff lane's lesson): every test + // compile now includes the harness-console-shim module, and the + // compiler's temp names are only unique within one process — + // concurrent compiles sharing a TMPDIR would clobber each + // other's shim .bc/.o + const compile_tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), "ejstest-compile-")); + const removeWrapper = function (): void { + if (wrapper_name != null) { + try { + fs.unlinkSync(wrapper_name); + } catch (e) {} + wrapper_name = null; + } + try { + fs.rmSync(compile_tmpdir, { recursive: true, force: true }); + } catch (e) {} + }; + const compiler = compilers[stage_to_run]; + if (compiler == null) throw new Error("bad stage " + stage_to_run); + const ccomp = spawn( + compiler, + platform_target.concat(extra_flags).concat(output_args).concat([ + "--srcdir", + "--moduledir", + "../node-compat", + "--moduledir", + "../ejs-llvm", + compile_target, + ]), + { env: Object.assign({}, process.env, { TMPDIR: compile_tmpdir }) } + ); + ccomp.on("exit", function (code) { + removeWrapper(); + if (code !== 0) { + const elapsed = getElapsed(start); + testFailed(test_name, `compiler failed (exit code = ${code})`, elapsed); + cb(); + return; + } + // XXX check code to make sure we were successful? + if (platform_to_test === "sim") { + process.env["EJS_FORCE_STDOUT"] = "1"; + process.env["DYLD_FRAMEWORK_PATH"] = + "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/System/Library/Frameworks:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/System/Library/PrivateFrameworks"; + process.env["DYLD_LIBRARY_PATH"] = + "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/usr/lib:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/usr/lib/system:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/System/Library/PrivateFrameworks/FontServices.framework:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/System/Library/Frameworks/Accelerate.framework/Frameworks/vecLib.framework:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk//System/Library/Frameworks/OpenGLES.framework"; + } + + const cexec = spawn("./" + test + ".exe"); + let test_stdout = ""; + cexec.on("close", function () { + stdouts[test_name] = test_stdout; + + // XXX check code to make sure we were successful? + + const elapsed = getElapsed(start); + checkStdout(test_name, elapsed, cb); + }); + cexec.on("error", function (err) { + const elapsed = getElapsed(start); + testFailed(test_name, err.toString(), elapsed); + cb(); + }); + cexec.stdout.on("data", function (msg) { + test_stdout += msg; + }); + cexec.stderr.on("data", function () {}); + }); + ccomp.on("error", function (err) { + removeWrapper(); + const elapsed = getElapsed(start); + testFailed(test_name, err.toString(), elapsed); + cb(); + return; + }); + } catch (e) { + console.log(e); + setTimeout(cb, 0); + return; + } + } +} + +function processTests( + gen_expected: boolean, + tests: string[], + cb: (err?: Error | null) => void +): void { + // (the old scheduler seeded i=test_threads but incremented i before + // reading tests[i] in the callback — the test at index test_threads + // was silently skipped in BOTH passes, which is how weakmap2.js ran + // on a years-stale baseline) + let next = 0; + let num_outstanding = 0; + + const launch = function (): void { + while (num_outstanding < test_threads && next < tests.length) { + const t = tests[next++]; + if (t == null) continue; + num_outstanding++; + processOneTest(gen_expected, t, function () { + num_outstanding--; + if (next >= tests.length && num_outstanding === 0) { + setTimeout(cb, 0); + return; + } + launch(); + }); + } + }; + launch(); +} + +function readTest(test: string): void { + const test_name = path.basename(test); + const contents = fs.readFileSync(test).toString(); + const lines = contents.split("\n"); + + // read the comments at the start, and pull out useful info + for (let i = 0, e = lines.length; i < e; i++) { + let line = lines[i]; + if (line == null || line.indexOf("//") !== 0) { + return; + } + + line = line.substr(2).trim(); + + if (line.indexOf("skip-if:") === 0) { + if (skip_ifs[test_name]) + throw new Error("test " + test + " already has a skip-if: directive"); + skip_ifs[test_name] = line.substr("skip-if:".length).trim(); + } + + if (line.indexOf("xfail:") === 0) { + if (xfails[test_name]) + throw new Error("test " + test + " already has a xfail: directive"); + xfails[test_name] = line.substr("xfail:".length).trim(); + } + + if (line.indexOf("generator:") === 0) { + if (generators[test_name]) + throw new Error("test " + test + " already has a generator: directive"); + generators[test_name] = line.substr("generator:".length).trim(); + } + } +} + +const args = process.argv.slice(2); + +let test_to_run: string | null = null; + +if (args[0] == "-p") { + args.shift(); + const p = args.shift(); + if (p == null) { + throw new Error("-p requires an argument [osx, sim]"); + } + platform_to_test = p; + if (platform_to_test !== "osx" && platform_to_test !== "sim") { + throw new Error("-p requires an argument [osx, sim]"); + } +} + +if (args[0] == "-s") { + args.shift(); + const s = args.shift(); + if (s == null) throw new Error("-s requires an argument between 0 and 3"); + stage_to_run = parseInt(s); + if (!(stage_to_run >= 0 && stage_to_run < compilers.length)) + throw new Error("-s requires an argument between 0 and 3"); +} +if (args[0] == "-t") { + args.shift(); + const t = args.shift(); + if (t == null) throw new Error("-t requires an argument (the test file to run)"); + test_to_run = t; + test_threads = 1; // XXX workaround for a bug, but we also only need 1 thread when we're running 1 test +} + +function runTests(tests: string[]): void { + tests.forEach(readTest); + + if (tests.length == 1) + console.log( + "running " + + tests[0] + + " against stage " + + stage_to_run + + " (" + + compilers[stage_to_run] + + ")" + ); + else + console.log( + "running " + + tests.length + + " tests against stage " + + stage_to_run + + " (" + + compilers[stage_to_run] + + ")" + ); + + processTests(true, tests, function (err) { + if (err) { + console.log(err); + process.exit(1); + } + processTests(false, tests, function () { + const run_failed = failed_tests.length > 0; + if (run_failed) { + console.log(); + console.log(testColumn(failed_tests.length + " failed tests")); + console.log(testColumn("================")); + failed_tests.forEach(function (t) { + console.log(testColumn(t)); + }); + } + console.log( + testColumn(" "), + resultColumn("done", result_types[run_failed ? "fail" : "pass"].colorizer) + ); + process.exit(run_failed ? -1 : 0); + }); + }); +} + +if (test_to_run) { + runTests([test_to_run]); +} else { + // run all the tests + + let tests = globSync("./*+([0-9]).js"); + runTests(tests); +} diff --git a/test/toLocaleString3.js b/test/toLocaleString3.js index 97910c89..7a58cb83 100644 --- a/test/toLocaleString3.js +++ b/test/toLocaleString3.js @@ -1,3 +1,5 @@ +// xfail: Number.prototype.toLocaleString lacks ICU's default maximumFractionDigits=3 rounding (node: 1.236, ejs: 1.2355). stale-baseline zombie flushed by runtime-P3 + var a = [1.2355, 1.2, "hi there", { a: 5 }]; console.log(a.toLocaleString()); diff --git a/test/tostring5.js b/test/tostring5.js index 59959245..a465899e 100644 --- a/test/tostring5.js +++ b/test/tostring5.js @@ -1,3 +1,5 @@ +// xfail: Date.prototype is an ordinary object in ES2015+ (node throws TypeError on Date.prototype.toString()); ejs still gives it a [[DateValue]]. stale-baseline zombie flushed by runtime-P3 + console.log("date"); console.log(Date.prototype.toString()); console.log("object date.proto.tostring"); diff --git a/test/tsconfig.json b/test/tsconfig.json new file mode 100644 index 00000000..c22cfe08 --- /dev/null +++ b/test/tsconfig.json @@ -0,0 +1,21 @@ +{ + // tester.ts (the suite runner) — compiled to tester.js in place by + // buck-test-stage.sh when the test tree is staged. Same strict + // family as the root tsconfig, but CommonJS output so plain node + // can run it. Hand-run: node ../node_modules/typescript/bin/tsc -p . + "compilerOptions": { + "strict": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "noEmitOnError": true, + "target": "es2016", + "module": "commonjs", + "esModuleInterop": true, + // glob's path-scurry .d.ts trips over @types/node 26 (Dirent + // gained parentPath); their problem, not ours + "skipLibCheck": true, + "types": ["node"], + "outDir": "." + }, + "files": ["tester.ts", "tester-deps.d.ts"] +} diff --git a/test/typedarray8.js b/test/typedarray8.js index e2d86ef4..5ca2aed0 100644 --- a/test/typedarray8.js +++ b/test/typedarray8.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm var buffer = new ArrayBuffer(8); var uint8 = new Uint8Array(buffer); diff --git a/test/typeof1.js b/test/typeof1.js index b1792f0f..c6f47995 100644 --- a/test/typeof1.js +++ b/test/typeof1.js @@ -1,7 +1,3 @@ -// generator: none -// we can't generate using babel-node since typeof null is 'object' -// under node (at least the versions we run against.) - console.log(typeof undefined); console.log(typeof null); console.log(typeof "hi"); diff --git a/test/types-argsink1.js b/test/types-argsink1.js new file mode 100644 index 00000000..3165507e --- /dev/null +++ b/test/types-argsink1.js @@ -0,0 +1,30 @@ +// sinking-P3 probe: rest_args/args_obj length sinking +// (docs/sinking-plan.md). Every line must match node exactly, with and +// without --types and under -fno-args-sink. + +function len0() { return arguments.length; } +function len2(a, b) { return arguments.length; } +function lenExpr(a) { return arguments.length - 1; } +console.log(len0(), len0(1), len2(), len2(1, 2, 3), lenExpr(1), lenExpr(1, 2, 3, 4)); + +function rl(a, ...r) { return r.length; } +console.log(rl(1), rl(1, 2), rl(1, 2, 3, 4)); + +// declining uses keep full semantics +function idx() { return arguments.length + ":" + arguments[0]; } +console.log(idx(), idx("x")); + +function fwd() { return Array.prototype.slice.call(arguments).join(","); } +console.log(fwd(1, 2, 3)); + +function restAll(...r) { return r.length + ":" + r.join("|"); } +console.log(restAll(), restAll(1, 2)); + +// arrow captures the enclosing arguments (env escape declines the sink) +function arrowCapture(a) { var g = () => arguments.length; return g(); } +console.log(arrowCapture(1, 2, 3)); + +// generator rest resolves in the outer function and rides the env +function* gen(...r) { yield r.length; yield r[0]; } +var it = gen(7, 8); +console.log(it.next().value, it.next().value); diff --git a/test/types-flowsink1.js b/test/types-flowsink1.js new file mode 100644 index 00000000..64a5b380 --- /dev/null +++ b/test/types-flowsink1.js @@ -0,0 +1,71 @@ +// sinking-P3 probe: flow-sensitive field writes + partial-escape +// materialization (docs/sinking-plan.md). Every line must match node +// exactly, with and without --types, under EJS_SHAPES=off, gc-stress, +// and -fno-flow-sink. + +function branches(c, x, y) { var o = { a: 0 }; if (c) o.a = x; else o.a = y; return o.a; } +console.log(branches(true, 1, 2)); +console.log(branches(false, 1, 2)); + +function loopAcc(n) { + var o = { sum: 0, count: 0 }; + for (var i = 0; i < n; i++) { o.sum = o.sum + i; o.count = o.count + 1; } + return o.sum + ":" + o.count; +} +console.log(loopAcc(0)); +console.log(loopAcc(10)); + +function readBeforeWrite(x) { var o = { a: 5 }; var r = o.a; o.a = x; return r + "," + o.a; } +console.log(readBeforeWrite(9)); + +// partial escape: the object materializes at the call; identity and +// mutation through the alias must behave exactly +var captured = null; +function capture(o) { captured = o; return o; } +function escapes(x) { + var o = { a: 1, b: 2 }; + o.a = x; + var r = capture(o); + return (r === captured) + ":" + captured.a + ":" + captured.b; +} +console.log(escapes(42)); +captured.a = 77; +console.log(captured.a); + +// escape via return: two calls yield distinct objects +function mk(a, b) { var o = { x: 0, y: 0 }; o.x = a; o.y = b; return o; } +var m1 = mk(1, 2), m2 = mk(1, 2); +console.log(m1.x, m1.y, m1 === m2); + +// a fresh object per iteration escapes each time +function loopEscape(n) { + var out = []; + for (var i = 0; i < n; i++) { var o = { v: 0 }; o.v = i; out.push(o); } + var s = ""; + for (var j = 0; j < out.length; j++) s += (j ? "," : "") + out[j].v; + return s + ":" + (out[0] === out[1]); +} +console.log(loopEscape(4)); + +// declined shapes keep exact semantics: key-adding write +function addsKey(x) { var o = { a: 1 }; o.b = x; return o.a + ":" + o.b; } +console.log(addsKey(3)); + +// write inside try +function tryWrite(x) { var o = { a: 1 }; try { o.a = x; } catch (e) { o.a = -1; } return o.a; } +console.log(tryWrite(8)); + +// self-reference declines +function selfRef() { var o = { a: null }; o.a = o; return o.a === o; } +console.log(selfRef()); + +// a setter installed on Object.prototype must intercept the (declined) +// key-adding write — the epoch-free soundness pin +Object.defineProperty(Object.prototype, "zz", { + set: function (v) { this._zz = v * 2; }, + get: function () { return this._zz; }, + configurable: true, +}); +function addsZZ(x) { var o = { a: 1 }; o.zz = x; return o.zz; } +console.log(addsZZ(21)); +delete Object.prototype.zz; diff --git a/test/types-sink1.js b/test/types-sink1.js new file mode 100644 index 00000000..0f110f56 --- /dev/null +++ b/test/types-sink1.js @@ -0,0 +1,12 @@ +function Point(x, y) { this.x = x; this.y = y; } +function alloc(n) { + var s = 0; var i = 0; + while (i < n) { + var p = new Point(i, i + 1); + s = s + p.x + p.y; + i = i + 1; + } + return s; +} +var o = { a: 1, b: 2 }; +console.log(alloc(200000) + o.a + o.b); diff --git a/test/types-sink2.js b/test/types-sink2.js new file mode 100644 index 00000000..ce8075c2 --- /dev/null +++ b/test/types-sink2.js @@ -0,0 +1,7 @@ +function f(n) { + var o = { a: n, b: n + 1 }; + return o.a + o.b; +} +var s = 0; var i = 0; +while (i < 100) { s = s + f(i); i = i + 1; } +console.log(s); diff --git a/test/types/README.md b/test/types/README.md new file mode 100644 index 00000000..187d2011 --- /dev/null +++ b/test/types/README.md @@ -0,0 +1,83 @@ +# --types probe census (Phase 3 / Phase 3.6) + +Probe files for the oracle-guided typed-arithmetic fast path +(docs/maam-plan.md, Phase 3) and for function specialization +(Phase 3.6). Like `test/modernization/`, these live +OUTSIDE the tester's `*.js` discovery glob in `test/` itself +(subdirectories are not scanned) and are runnable standalone: compile one +with the node-hosted compiler and `--types`, run it, and diff stdout +against `node ` (color-free: `NO_COLOR=1`, `FORCE_COLOR` unset). +The whole-suite behavioral gate is `./buck-test-types-diff.sh`. + +`diamonds=N` below is the count from the `--types` stats line — how many +guarded has_tag/f64 diamonds lowering emitted for the file. The guard +makes every diamond correct regardless of oracle accuracy; these probes +document where the fast path FIRES. + +Census as of 2026-07-22 (echojs @ 568efc7, maam @ 8d6a157): + +| probe | shape | diamonds | vs node | +|---|---|---|---| +| types-locals1 | pure numeric locals (`+ - * / <`) | 6 | match | +| types-params1 | numeric params, module-local call sites (incl. 1/0 → Infinity through the fast fdiv) | 4 | match | +| types-literals1 | literals mixed with typed vars (incl. the unary-minus literal parse `x - -2`) | 5 | match | +| types-widen1 | reassignment widening: num→str and undefined→num bindings do NOT diamond (documented; only exact {number} qualifies) | 0 | match | +| types-loops1 | for/while counters, `<` in loop conditions | 6 | match | +| types-wrongoracle1 | the wrong-oracle guard: lib.js types `inc`'s param {number} from its only local call, main calls `inc("x")` cross-module → slow path, "x1" (since runtime-P2 lib also reports `specWrapped=1` — the exported inc gets the boundary wrapper; the string still routes generic through its guard chain) | 2 (in lib) | n/a¹ | +| types-bench1 | the Phase 3 microbenchmark kernel (adds/muls/divs/compares over typed locals) | 9 | match | +| types-spec1 | Phase 3.6 specialization: module-local looping kernel → f64(f64) clone, exact-arity sites rewritten to call_typed (`specialized=1 specSites=2`); the extra-arg site stays generic | 6 | match | +| types-spec2 | Phase 3.6 cross-function specialization (the hypot2-demo shape): hypot2 called only inside sum, prefix-safe toplevel slot stores → both clone, all four sites rewrite incl. the one inside sum$typed (`specialized=2 specSites=4`) | 7 | match | +| types-specescape1 | Phase 3.6 escape rejection: f LOOKS numeric-closed but its closure is passed as a call argument → NOT trusted-specialized (`specialized=0`); since runtime-P2 the escapee gets the boundary wrapper instead (`specWrapped=1`), and the escaped string call fails its guard chain onto the generic path | 5 | match | + +Shapes probes (shapes-plan P4.3; `shapeGuards=N` from the stats line +counts has_shape diamonds the way `diamonds=N` counts has_tag ones): + +| probe | shape | shapeGuards | vs node | +|---|---|---|---| +| types-bench2 | the object-model microbenchmark: monomorphic constructor + p.x/p.y kernel; guarded fast paths + shape-region merging (`shapeGuards=10`, 2 shape regions merged); 2026-07-24 numbers: --types 3.06s vs flag-off 6.56s (2.1×), vs EJS_SHAPES=off 5.82s (~1.9× shapes-attributable); P4.4 born-with-shape (`ctorFills=1`) takes it to **2.03s** vs flag-off 6.76s (3.3×) | 10 | match | +| types-shapeswrong1 | the wrong-oracle shape guard: lib types sumxy's receiver {x: num, y: num} from its one local call; main hands it a repr-mismatched object ("ab"), an extra-field object, and a dictionary-mode (post-delete) object → all route slow with node-identical values; the matching Point goes fast | 4 (in lib) | n/a¹ | +| types-bornshape1 | born-with-shape (P4.4): a static literal is make_object_shaped, the Pt ctor prefix is the empty-shape-guarded fill (`bornShaped=1 ctorFills=1`); keys order, `in`, growth past the born shape, and a repr-differing construction all match node | 0 | match | +| types-bornshapewrong1 | P4.4 edge cases: a reused non-empty receiver (guard fails), an `in`-cut fence, a frozen receiver (runtime re-check), a proto-chain SETTER intercepting the batched store, and a non-writable proto data prop — every one routes sequential with node-identical output (`bornShaped=3 ctorFills=3 fenceDeclined=short-prefix:1`); also found the provenNumberIntrinsic const-join gap (which P4.5's typed stores later dissolved entirely) | 0 | match | +| types-typedslots1 | typed slots (P4.5): the fused kernel fast on the matching shape, slow on repr-mismatched / extra-field / dictionary receivers; -0 (1/x sign), NaN, Infinity bit-survival through raw slot store→load; a mid-kernel repr-flip transition (string into an f64 field) and the boxed-field store paths (`shapeGuards=9 shapeTyped=loads:7,stores:1 bornShaped=3 ctorFills=2`); node-identical incl. under EJS_SHAPES=off and EJS_GC_EVERY_N_ALLOC=7 | 9 | match | +| types-bench3 | the P4.6 polymorphic microbenchmark: the bench2 kernel with two receiver classes ({x,y} / {z,x,y}) alternating at one site — the 2-way guard chain (`shapePolyGuards=4`) runs 0.31s, PARITY with the monomorphic twin, vs 1.67s declined (-fno-poly-shape-guards) and 3.64s flag-off (2026-07-24, M-series) | 4 (poly) | match | +| types-poly1 | the wrong-oracle probe for the 2-way chain: lib's oracle types sum/setx's receiver with BOTH terminal shapes from local calls (`shapePolyGuards=4 shapeTyped=loads:6,stores:2`); cross-module receivers it never saw — repr-mismatched, a third shape, dictionary-mode (post-delete) — all route through the shared slow path; identical output incl. under EJS_SHAPES=off and EJS_GC_EVERY_N_ALLOC=7 | 4 (in lib) | n/a¹ | + +P4.6 evidence probes (extensions measured and NOT landed; the numbers +and rationale live in shapes-plan.md's P4.6 entry): + +| probe | shape | vs node | +|---|---|---| +| types-accessor1 | proto-getter dispatch kernel, 20M `p.len2` reads (accessor inlining: ~7× headroom recorded, DECLINED pending proto-guard soundness machinery) | match | +| types-array1 | dense-array element kernel, 20M `a[j]` reads (element shapes: 2.4× headroom vs flag-off recorded, DEFERRED — arrays are outside shaped mode) | match | + +runtime-P2 probes (export-boundary wrapper + escape-taint fence, +2026-07-29; `specWrapped`/`specFenced` from the stats line count +boundary wrappers installed and call sites the taint fence kept +generic): + +| probe | shape | stats | vs node | +|---|---|---|---| +| types-wrapper1 | the exported kernel: never trusted-specialized, but wrapped — has_tag guards at the generic entry dispatch to an UNTRUSTED guarded f64 clone (folds structurally from the entry boxes). Cross-module number calls take the clone; a string and a missing arg fail the chain onto the generic body | `specWrapped=1` (in lib) | n/a¹ | +| types-wrapperfence1 | the escape-taint fence: module-private g looks closed-world numeric but one call site is hosted in the exported f; maam's constant-propagation domain prunes g's `y>5` branch under the analyzed 3, so a trusted rewrite of that site would unbox `"s"` unguarded on f(7) — the fence keeps it generic (f(7) → NaN, node-identical); the init-time site still rewrites to g$typed | `specialized=1 specSites=1 specFenced=1 specRejected=1`² (in lib) | n/a¹ | +| types-bench5 | the types-bench1 workload with the kernel EXPORTED and called cross-module: flag-off 0.34 s → 0.07 s user with the wrapper (~4.9×), PARITY with types-bench1's closed-world trusted path (0.07 s) — the module boundary costs one has_tag per formal per call | `specWrapped=1` (in lib) | n/a¹ | + +² the `specRejected` there is f's own wrapper declining on the payoff +check (its body is a bare delegation call — no diamonds to fold), not a +failure. + +¹ node cannot execute this file's bare-ESM import layout from test/; +the check here is flag-off vs `--types` executables producing identical +output (verified — and the slow-path routing is the probe's point). + +Wider context (the `--types` diff lane over all of `test/`, 2026-07-22): +458 files, 457 identical flag-off vs `--types`, 0 divergent, 1 N/A +(tester.js, esprima parse gap), 67 diamonds total across the suite. +Suite files are string/object-heavy by design — the diamond count is +expected to be modest outside numeric kernels. + +P4.3 re-run (2026-07-24, shapes guards live): 459 files, 458 identical, +0 divergent, 1 N/A (tester.js), 78 diamonds. Shape telemetry across +the suite: 13,154 access sites consulted, 809 guarded; declines: +unmapped 7,575 / capped 4,287 / empty 269 / no-field 194 / +polymorphic 12 / union-repr 8 — same story: guards fire in kernels, +the string-heavy suite mostly declines (visibly, per reason). diff --git a/test/types/types-accessor1.js b/test/types/types-accessor1.js new file mode 100644 index 00000000..09f98ff8 --- /dev/null +++ b/test/types/types-accessor1.js @@ -0,0 +1,31 @@ +// the shapes-plan P4.6 accessor-inlining EVIDENCE probe (the extension +// was measured and DECLINED — see the plan's P4.6 entry). defineProperty +// (not a getter literal — those are a maam NormalizeError) installs a +// proto getter; every p.len2 is an accessor dispatch through the generic +// get, and p.len2 correctly declines "no-field" (the accessor is not in +// the receiver's shape). 2026-07-24 numbers (M-series): 2.31s --types / +// 5.44s flag-off / 0.06s node; the same arithmetic through guarded slots +// runs 0.32s (~7x headroom). Sound inlining needs proto-identity or +// proto-shape guards (a receiver has_shape proves nothing about the +// dictionary-mode proto carrying the getter) — a designed phase, not a +// measured extension. +function Pt(x, y) { this.x = x; this.y = y; } +Object.defineProperty(Pt.prototype, "len2", { + get: function () { return this.x * this.x + this.y * this.y; } +}); +function kern(p, n) { + var s = 0; + var i = 0; + while (i < n) { + s = s + p.len2; + i = i + 1; + } + return s; +} +var out = 0; +var r = 0; +while (r < 20) { + out = out + kern(new Pt(r, r + 1), 1000000); + r = r + 1; +} +console.log(out); diff --git a/test/types/types-array1.js b/test/types/types-array1.js new file mode 100644 index 00000000..2853124d --- /dev/null +++ b/test/types/types-array1.js @@ -0,0 +1,27 @@ +// the shapes-plan P4.6 array-element-shapes EVIDENCE probe (the +// extension was measured and DEFERRED — see the plan's P4.6 entry). +// a[j] is a computed member — no shape machinery applies (arrays are +// exotics outside shaped mode, maam smashes element types). 2026-07-24 +// numbers (M-series, 20M reads): 0.57s --types / 1.38s flag-off / +// 0.06s node — real headroom, owned by a future typed-element-storage +// phase alongside the gc-plan work. +function kern(a, n) { + var s = 0; + var r = 0; + while (r < n) { + var j = 0; + while (j < 64) { + s = s + a[j]; + j = j + 1; + } + r = r + 1; + } + return s; +} +var arr = []; +var k = 0; +while (k < 64) { + arr.push(k * 1.5); + k = k + 1; +} +console.log(kern(arr, 312500)); diff --git a/test/types/types-bench1.js b/test/types/types-bench1.js new file mode 100644 index 00000000..0d0d7ba3 --- /dev/null +++ b/test/types/types-bench1.js @@ -0,0 +1,19 @@ +// the Phase 3 arithmetic microbenchmark kernel: tight loop of adds/muls/ +// divs/compares over module-local {number} locals — everything the +// oracle can type, nothing else. Also serves as a probe. +function kernel(n) { + var s = 0; + var i = 0; + while (i < n) { + s = s + i * i - i / 2; + i = i + 1; + } + return s; +} +var out = 0; +var r = 0; +while (r < 40) { + out = out + kernel(1000000); + r = r + 1; +} +console.log(out); diff --git a/test/types/types-bench2.js b/test/types/types-bench2.js new file mode 100644 index 00000000..16a787cf --- /dev/null +++ b/test/types/types-bench2.js @@ -0,0 +1,38 @@ +// the shapes-plan P4.3 object-model microbenchmark kernel — the twin of +// types-bench1: allocate N points through a monomorphic constructor and +// sum p.x*p.x + p.y*p.y, so the residual wall time is property access. +// The oracle types kern's parameter (and the module-local point) with the +// single terminal shape {x: num, y: num}; every p.x / p.y lowers to a +// has_shape diamond whose fast arm is a fixed-slot load. Also serves as +// a probe: shapeGuards on the stats line counts the emitted diamonds. +function Point(x, y) { + this.x = x; + this.y = y; +} +function kern(p, n) { + var s = 0; + var i = 0; + while (i < n) { + s = s + p.x * p.x + p.y * p.y; + i = i + 1; + } + return s; +} +function alloc(n) { + var s = 0; + var i = 0; + while (i < n) { + var p = new Point(i, i + 1); + s = s + p.x + p.y; + i = i + 1; + } + return s; +} +var out = 0; +var r = 0; +while (r < 20) { + out = out + kern(new Point(3, 4), 1000000); + out = out + alloc(200000); + r = r + 1; +} +console.log(out); diff --git a/test/types/types-bench3.js b/test/types/types-bench3.js new file mode 100644 index 00000000..0ef89ed5 --- /dev/null +++ b/test/types/types-bench3.js @@ -0,0 +1,27 @@ +// the shapes-plan P4.6 polymorphic microbenchmark: the types-bench2 +// kernel with TWO receiver classes alternating at one site ({x,y} and +// {z,x,y} — neither a transition-prefix of the other, and the shared +// fields at different slots). The oracle reports both terminal shapes; +// the 2-way guard chain gives each class a fixed-slot fast arm. +// 2026-07-24 numbers (M-series): 0.31s with the chain — parity with the +// monomorphic twin — vs 1.67s declined (-fno-poly-shape-guards) and +// 3.64s flag-off. +function P2(x, y) { this.x = x; this.y = y; } +function P3(x, y, z) { this.z = z; this.x = x; this.y = y; } +function kern(p, n) { + var s = 0; + var i = 0; + while (i < n) { + s = s + p.x * p.x + p.y * p.y; + i = i + 1; + } + return s; +} +var out = 0; +var r = 0; +while (r < 20) { + out = out + kern(new P2(r, r + 1), 500000); + out = out + kern(new P3(r + 2, r + 3, r), 500000); + r = r + 1; +} +console.log(out); diff --git a/test/types/types-bench4.js b/test/types/types-bench4.js new file mode 100644 index 00000000..1f0c767c --- /dev/null +++ b/test/types/types-bench4.js @@ -0,0 +1,41 @@ +// the sinking-P3 flow-sink microbenchmark: a loop-accumulator OBJECT +// whose fields are read and written every iteration. With +// flow-sensitive sinking the object scalar-replaces into loop-carried +// values (allocation-free, memory-op-free); without it every iteration +// pays the read/write diamonds against a real heap object. A/B: +// -fno-flow-sink at compile time. +function accum(n) { + var o = { sum: 0, weighted: 0, count: 0 }; + var i = 0; + while (i < n) { + o.sum = o.sum + i; + o.weighted = o.weighted + i * 0.5; + o.count = o.count + 1; + i = i + 1; + } + return o.sum + o.weighted + o.count; +} +// the partial-escape twin: the accumulator escapes at the end of every +// call — materialization keeps the loop allocation-free and pays one +// allocation per call +var last = null; +function keep(o) { last = o; } +function accumEscape(n) { + var o = { sum: 0, count: 0 }; + var i = 0; + while (i < n) { + o.sum = o.sum + i; + o.count = o.count + 1; + i = i + 1; + } + keep(o); + return 1; +} +var out = 0; +var r = 0; +while (r < 20) { + out = out + accum(1000000); + out = out + accumEscape(1000000); + r = r + 1; +} +console.log(out, last.sum, last.count); diff --git a/test/types/types-bench5.js b/test/types/types-bench5.js new file mode 100644 index 00000000..08209eae --- /dev/null +++ b/test/types/types-bench5.js @@ -0,0 +1,11 @@ +// runtime-P2 microbenchmark driver: the types-bench1 workload, but the +// kernel lives in another module and is reached through its export — +// every call crosses the module boundary into the wrapper. +import { kernel } from "./types-bench5/lib"; +var out = 0; +var r = 0; +while (r < 40) { + out = out + kernel(1000000); + r = r + 1; +} +console.log(out); diff --git a/test/types/types-bench5/lib.js b/test/types/types-bench5/lib.js new file mode 100644 index 00000000..8fd62072 --- /dev/null +++ b/test/types/types-bench5/lib.js @@ -0,0 +1,16 @@ +// runtime-P2 microbenchmark: the types-bench1 kernel, EXPORTED. The +// export pins the trusted path (the closure escapes through the +// non-promoted slot), so before runtime-P2 every cross-module call ran +// the fully generic body; the boundary wrapper recovers the typed +// kernel behind two per-call has_tag checks. +export function kernel(n) { + var s = 0; + var i = 0; + while (i < n) { + s = s + i * i - i / 2; + i = i + 1; + } + return s; +} +// module-local numeric profile for the oracle +console.log(kernel(100)); diff --git a/test/types/types-bornshape1.js b/test/types/types-bornshape1.js new file mode 100644 index 00000000..9c173655 --- /dev/null +++ b/test/types/types-bornshape1.js @@ -0,0 +1,21 @@ +// born-with-shape probe (shapes-plan P4.4): statically-keyed literals +// lower to make_object_shaped, fenced constructor prefixes to the +// empty-shape-guarded fill_object_shaped — stdout must match node +// exactly, including enumeration order, `in` results, and growth past +// the born shape. Stats line: bornShaped/ctorFills counts. +function Pt(x, y) { this.x = x; this.y = y; } +var p = new Pt(1, 2); +console.log(p.x + p.y); +console.log(Object.keys(p).join(",")); + +var lit = { a: 1, b: "s", c: true }; +console.log(Object.keys(lit).join(",")); +console.log(lit.a + lit.b); + +p.tag = "t"; // grow past the born shape (a plain transition) +console.log(Object.keys(p).join(",")); +console.log(("x" in p) + ":" + ("z" in p)); + +var mixed = new Pt("s", 2); // reprs differ from the candidate: still correct +console.log(mixed.x + mixed.y); +console.log(Object.keys(mixed).join(",")); diff --git a/test/types/types-bornshapewrong1.js b/test/types/types-bornshapewrong1.js new file mode 100644 index 00000000..a4f39c0f --- /dev/null +++ b/test/types/types-bornshapewrong1.js @@ -0,0 +1,43 @@ +// born-with-shape wrong/edge cases (shapes-plan P4.4): the empty-shape +// guard and the runtime re-checks route every off-script construction +// through the sequential path with node-identical behavior. +function Pt(x, y) { this.x = x; this.y = y; } + +// a reused non-empty receiver: the guard fails, sequential stores run +var reuse = { z: 9 }; +Pt.call(reuse, 1, 2); +console.log(reuse.z + reuse.x + reuse.y); +console.log(Object.keys(reuse).join(",")); + +// `in` mid-construction cuts the fence at compile time +function Probe(x, y) { + this.a = ("b" in this) ? 1 : 0; + this.b = y; +} +var q = new Probe(5, 6); +console.log(q.a + ":" + q.b); + +// a non-extensible receiver: the runtime re-check falls back, and the +// sequential [[Set]]s fail silently exactly like node (sloppy mode) +var frozen = Object.freeze({}); +Pt.call(frozen, 7, 8); +console.log("" + ("x" in frozen)); + +// a proto-chain SETTER must intercept the batched assignment (the +// shaped_proto_intercepts fallback): hijack captures x, y stores own. +// (defineProperty, not an accessor literal — a getter/setter literal is +// a maam NormalizeError and would kill the oracle for the whole module, +// leaving nothing born-shaped to test.) +function P2(x, y) { this.x = x; this.y = y; } +P2.prototype = {}; +Object.defineProperty(P2.prototype, "x", { + set: function (v) { this.hijack = v; } +}); +var h = new P2(1, 2); +console.log(h.hijack + ":" + h.x + ":" + h.y); + +// a non-writable proto data property silently swallows the own-store +function P3(a, b) { this.a = a; this.b = b; } +P3.prototype = Object.freeze({ a: 99 }); +var w = new P3(1, 2); +console.log(("a" in w) + ":" + w.a + ":" + w.b); diff --git a/test/types/types-ctorsink1.js b/test/types/types-ctorsink1.js new file mode 100644 index 00000000..5361c53d --- /dev/null +++ b/test/types/types-ctorsink1.js @@ -0,0 +1,60 @@ +// constructor-result sinking probe (docs/sinking-plan.md): the alloc +// kernel virtualizes behind the accessor-epoch check, and the epoch +// must retire it the moment anything intercept-capable lands on the +// prototype chain. The interceptors are installed through +// Object.prototype — installing through Point.prototype would already +// decline the sink statically (the ctor's loads must all be callees), +// so this file exercises the RUNTIME half of the contract: a clean run +// first, then a mid-loop accessor install, then a mid-loop non-writable +// data install, each byte-compared against node. (defineProperty, not +// accessor literals — the oracle can't normalize the latter.) +function Point(x, y) { + this.x = x; + this.y = y; +} + +function run(n, flip, installer) { + var s = 0; + var i = 0; + while (i < n) { + if (i === flip) installer(); + var p = new Point(i, i + 1); + s = s + p.x + p.y; + i = i + 1; + } + return s; +} + +function nothing() {} + +function installAccessor() { + Object.defineProperty(Object.prototype, "x", { + configurable: true, + set: function (v) { + this.hx = v * 100; + }, + get: function () { + return this.hx + 7; + }, + }); +} + +function installFrozenData() { + Object.defineProperty(Object.prototype, "y", { + configurable: true, + value: 4242, + writable: false, + }); +} + +// clean epoch: the virtual arm runs the whole loop +console.log(run(1000, -1, nothing)); +// accessor lands at i===5: constructions from there on are intercepted +// (this.x = v stores hx, p.x reads hx + 7) +console.log(run(1000, 5, installAccessor)); +// still installed on later runs +console.log(run(10, -1, nothing)); +// a non-writable data property also intercepts: this.y = v is silently +// swallowed and p.y reads the prototype's 4242 +console.log(run(1000, 7, installFrozenData)); +console.log(run(10, -1, nothing)); diff --git a/test/types/types-ctorsink2.js b/test/types/types-ctorsink2.js new file mode 100644 index 00000000..ef1154a5 --- /dev/null +++ b/test/types/types-ctorsink2.js @@ -0,0 +1,58 @@ +// constructor-result sinking, the pure-win shape (docs/sinking-plan.md): +// a monomorphic alloc kernel with no interference anywhere — the +// canonical reduction is an allocation-free loop. Also exercises the +// declines around it: a site whose result escapes keeps its construct, +// and a ctor whose prototype is touched anywhere declines wholesale. +function Point(x, y) { + this.x = x; + this.y = y; +} + +function alloc(n) { + var s = 0; + var i = 0; + while (i < n) { + var p = new Point(i, i + 1); + s = s + p.x + p.y; + i = i + 1; + } + return s; +} + +// Escaper's result flows into a call: that site must keep its construct +function sink2_keep(p) { + return p.x; +} +function escaper(n) { + var s = 0; + var i = 0; + while (i < n) { + s = s + sink2_keep(new Point(i, i)); + i = i + 1; + } + return s; +} + +// Touched's prototype carries a method: the load discipline declines +// every Touched construct (a swapped or decorated prototype is exactly +// what the static screen exists for), and the method keeps working +function Touched(x, y) { + this.x = x; + this.y = y; +} +Touched.prototype.sum = function () { + return this.x + this.y; +}; +function methods(n) { + var s = 0; + var i = 0; + while (i < n) { + s = s + new Touched(i, i + 1).sum(); + i = i + 1; + } + return s; +} + +console.log(alloc(100000)); +console.log(escaper(1000)); +console.log(methods(1000)); diff --git a/test/types/types-literals1.js b/test/types/types-literals1.js new file mode 100644 index 00000000..ab874c8e --- /dev/null +++ b/test/types/types-literals1.js @@ -0,0 +1,8 @@ +// literals mixed with typed vars: literals are oracle-unmapped glue but +// type directly in lowering (incl. the unary-minus parse of -2) +var x = 10; +console.log(x + 1); +console.log(x * 2); +console.log(x - -2); +console.log(x / 4); +console.log(x < 100); diff --git a/test/types/types-locals1.js b/test/types/types-locals1.js new file mode 100644 index 00000000..ac7f703c --- /dev/null +++ b/test/types/types-locals1.js @@ -0,0 +1,9 @@ +// pure numeric locals: every binary op below is diamond-eligible +var a = 3; +var b = 4; +var c = a * a + b * b; +var d = c / 5; +console.log(c); +console.log(d); +console.log(a - b); +console.log(a < b); diff --git a/test/types/types-loops1.js b/test/types/types-loops1.js new file mode 100644 index 00000000..f3a07c3b --- /dev/null +++ b/test/types/types-loops1.js @@ -0,0 +1,10 @@ +// for-loop counters and < in loop conditions +var total = 0; +for (var i = 0; i < 10; i = i + 1) { + total = total + i * i; +} +var j = 0; +while (j < 5) { j = j + 1; } +console.log(total); +console.log(j); +console.log(i < j); diff --git a/test/types/types-params1.js b/test/types/types-params1.js new file mode 100644 index 00000000..4319d5da --- /dev/null +++ b/test/types/types-params1.js @@ -0,0 +1,7 @@ +// numeric params, module-local call sites: the oracle sees every call, +// types the params {number}, and the bodies diamond +function hyp2(x, y) { return x * x + y * y; } +function scale(v, k) { return v / k; } +console.log(hyp2(3, 4)); +console.log(scale(hyp2(6, 8), 4)); +console.log(scale(1, 0)); // Infinity through a real fdiv fast path diff --git a/test/types/types-poly1.js b/test/types/types-poly1.js new file mode 100644 index 00000000..a1bb27f4 --- /dev/null +++ b/test/types/types-poly1.js @@ -0,0 +1,18 @@ +// the wrong-oracle probe for the P4.6 2-way polymorphic chain: lib.js's +// oracle typed sum/setx's receiver with TWO terminal shapes from its +// module-local calls, so both classes ride their own fast arm. Cross- +// module we hand the chain receivers it never saw — a repr-mismatched +// object, a third shape, a dictionary-mode (post-delete) object — and +// every one must route through the shared slow path with node-identical +// output; correctness never depends on the oracle being right. +import { sum, setx, mk2, mk3 } from "./types-poly1/lib"; +console.log(sum(mk2(1, 2))); // arm-1 fast: 3 +console.log(sum(mk3(10, 20, 30))); // arm-2 fast: 30 +console.log(sum({ x: "a", y: "b" })); // repr mismatch: slow, "ab" +console.log(sum({ x: 1, y: 2, w: 3 })); // a third shape: slow, 3 +var del = { x: 100, y: 200 }; +delete del.x; del.x = 7; // dictionary mode: guards fail +console.log(sum(del)); // 207 +console.log(setx(mk3(1, 2, 3), 42)); // arm-2 typed store: 42 +console.log(setx({ x: "s", y: 0 }, "t")); // non-number into the chain: slow, "t" +console.log(setx(del, 9)); // dictionary store: slow, 9 diff --git a/test/types/types-poly1/lib.js b/test/types/types-poly1/lib.js new file mode 100644 index 00000000..92382659 --- /dev/null +++ b/test/types/types-poly1/lib.js @@ -0,0 +1,14 @@ +// the oracle sees TWO terminal shapes for sum's receiver — P2 {x,y} and +// P3 {z,x,y}, distinct classes whose shared fields sit at different +// slots — so p.x / p.y lower to the P4.6 2-way guard chain: each class +// takes its own fast arm, everything else shares one generic slow path. +function P2(x, y) { this.x = x; this.y = y; } +function P3(x, y, z) { this.z = z; this.x = x; this.y = y; } +export function sum(p) { return p.x + p.y; } +export function setx(p, v) { p.x = v; return p.x; } +export function mk2(x, y) { return new P2(x, y); } +export function mk3(x, y, z) { return new P3(x, y, z); } +console.log(sum(mk2(1, 2))); // 3 — types the receiver with P2... +console.log(sum(mk3(10, 20, 5))); // 30 — ...and with P3 +console.log(setx(mk2(3, 4), 7)); // 7 (typed store, arm 1) +console.log(setx(mk3(5, 6, 7), 8)); // 8 (typed store, arm 2) diff --git a/test/types/types-shapeswrong1.js b/test/types/types-shapeswrong1.js new file mode 100644 index 00000000..9fc455dc --- /dev/null +++ b/test/types/types-shapeswrong1.js @@ -0,0 +1,16 @@ +// the wrong-oracle shape guard (shapes-plan P4.3): lib.js's oracle typed +// sumxy's receiver with the terminal shape {x: num, y: num} from its only +// module-local call, but cross-module we hand it (a) a string-valued +// object with different reprs, (b) an object with extra fields, (c) a +// dictionary-mode object (post-delete), and (d) the shape-matching case. +// Every access must route through the guard (fast only when the runtime +// shape matches) with node-identical output — correctness never depends +// on the oracle being right. +import { sumxy, mk } from "./types-shapeswrong1/lib"; +console.log(sumxy({ x: "a", y: "b" })); // repr mismatch: slow, "ab" +var wide = { x: 10, y: 20, z: 30 }; +console.log(sumxy(wide)); // extra field: guard fails, 30 +var del = { x: 100, y: 200 }; +delete del.x; del.x = 7; // dictionary mode: guard fails +console.log(sumxy(del)); +console.log(sumxy(mk(3, 4))); // the matching shape: fast, 7 diff --git a/test/types/types-shapeswrong1/lib.js b/test/types/types-shapeswrong1/lib.js new file mode 100644 index 00000000..01621f06 --- /dev/null +++ b/test/types/types-shapeswrong1/lib.js @@ -0,0 +1,7 @@ +// the oracle here sees ONE terminal shape for sumxy's receiver — the +// module-local Point instances {x: num, y: num} — so p.x / p.y lower to +// has_shape diamonds against that shape. +function Point(x, y) { this.x = x; this.y = y; } +export function sumxy(p) { return p.x + p.y; } +export function mk(x, y) { return new Point(x, y); } +console.log(sumxy(mk(1, 2))); // the call that types the receiver diff --git a/test/types/types-spec1.js b/test/types/types-spec1.js new file mode 100644 index 00000000..f27a2be3 --- /dev/null +++ b/test/types/types-spec1.js @@ -0,0 +1,19 @@ +// Phase 3.6 probe: function specialization. kernel is module-local +// (promoted slot, never exported), numeric-only, and too big for EIR +// inlining (multi-block loop) — the local-closed-world analysis clones +// it as f64(f64) and rewrites the exact-arity toplevel call sites to +// call_typed. The extra-arg site stays on the generic path (still +// enumerated, still correct). specialized=1 specSites=2. +function kernel(n) { + var s = 0; + var i = 0; + while (i < n) { + s = s + i * i - i / 2; + i = i + 1; + } + return s; +} +var a = kernel(10); +var b = kernel(20); +var c = kernel(30, 99); // extra arg: generic site +console.log(a + ":" + b + ":" + c); diff --git a/test/types/types-spec2.js b/test/types/types-spec2.js new file mode 100644 index 00000000..7188ceba --- /dev/null +++ b/test/types/types-spec2.js @@ -0,0 +1,16 @@ +// Phase 3.6 probe: CROSS-FUNCTION specialization (the hypot2-demo +// shape). Both functions are module-local; hypot2's only calls live +// inside sum, whose slot store sits in the toplevel entry prefix (no +// CALL-effect instruction before it), so the cross-function loads +// rewrite too — including the one inside sum's own clone (the pass's +// fixpoint round). specialized=2 specSites=4 (two toplevel sum calls, +// hypot2 in generic sum, hypot2 in sum$typed). +function hypot2(a, b) { + return a * a + b * b; +} +function sum(n) { + var total = 0; + for (var i = 0; i < n; i = i + 1) total = total + hypot2(i, i + 1); + return total; +} +console.log(sum(1000) + ":" + sum(2000)); diff --git a/test/types/types-specescape1.js b/test/types/types-specescape1.js new file mode 100644 index 00000000..51d4deae --- /dev/null +++ b/test/types/types-specescape1.js @@ -0,0 +1,26 @@ +// Phase 3.6 probe: the wrong-oracle discipline for specialization. f +// LOOKS closed-world numerically (all direct calls pass numbers), but +// its closure also escapes as a call ARGUMENT — the structural escape +// analysis must reject the TRUSTED clone (specialized=0). Since +// runtime-P2 the escapee gets the boundary wrapper instead +// (specWrapped=1): has_tag guards at the generic entry dispatch to a +// guarded trust-free clone, so via()'s string call fails the guard +// chain and runs the generic body. Behavior must be identical to +// flag-off; note via() really does call f with a string, which the +// generic path handles (numeric string concat semantics preserved). +function f(n) { + var s = 0; + var i = 0; + while (i < n) { + s = s + i; + i = i + 1; + } + return s; +} +function via(g, x) { + return g(x); +} +var direct = f(10); +var indirect = via(f, 5); +var mixed = via(f, "3"); // a string reaches f only through the escape +console.log(direct + ":" + indirect + ":" + mixed); diff --git a/test/types/types-typedslots1.js b/test/types/types-typedslots1.js new file mode 100644 index 00000000..a579228f --- /dev/null +++ b/test/types/types-typedslots1.js @@ -0,0 +1,68 @@ +// typed slots (shapes-plan P4.5): f64-repr slots are accessed RAW inside +// guard regions — slot_load produces a raw f64, slot_store consumes one, +// and the heterogeneous merge fuses shape + numeric regions so the kernel +// below runs one has_shape guard, raw loads, and raw arithmetic with one +// generic slow path. The probe pins the semantics the raw flow must not +// disturb: +// - the fused kernel on the matching shape (fast) and on repr-mismatched +// / extra-field / dictionary-mode receivers (slow) — same values; +// - bit-level observables through raw slot traffic: -0 (1/x sign), NaN, +// Infinity survive store→load round trips; +// - an f64-field store of a number takes the typed fast path; storing a +// string into the same field is a repr TRANSITION (generic path) and +// later reads guard-fail to the slow path — values stay node-identical; +// - a boxed-field store of a non-number stays on its (boxed) fast path. +function Pt(x, y) { + this.x = x; + this.y = y; +} +function kern(p) { + return p.x * p.x + p.y * p.y; +} +function getx(p) { + return p.x; +} +function setx(p, v) { + p.x = v; + return p.x; +} +console.log(kern(new Pt(3, 4))); // fast: 25 +console.log(getx({ x: "a", y: "b" })); // repr mismatch: slow read, "a" +// (string * string is a standing runtime gap — ejs-ops.c _ejs_op_mult — +// so repr-mismatched receivers are exercised through reads, not kern) +var wide = { x: 1, y: 2, z: 3 }; +console.log(kern(wide)); // extra field: slow, 5 +var del = { x: 5, y: 6 }; +delete del.x; +del.x = 5; +console.log(kern(del)); // dictionary mode: slow, 61 + +// bit-level observables through raw slot traffic +var q = new Pt(-0, 0 / 0); +console.log(1 / q.x); // -Infinity (the -0 survived) +console.log(q.y === q.y); // false (NaN survived) +console.log(setx(q, 1 / 0)); // Infinity through the typed store +console.log(1 / setx(q, -0)); // -Infinity through the typed store + +// repr transition: the typed store's has_tag guard routes the string to +// the generic path, which transitions x to boxed; later typed reads +// guard-fail (shape changed) and stay correct +var t = new Pt(1, 2); +console.log(setx(t, "s")); // "s" (transition, generic) +console.log(t.x + t.y); // "s2" (guard-failing typed read) +console.log(setx(t, 9)); // 9 (x now boxed-repr: generic again) +console.log(t.x + t.y); // 11 + +// a boxed field keeps its boxed fast path for non-numbers +function Tag(name, v) { + this.name = name; + this.v = v; +} +function rename(o, s) { + o.name = s; + return o.name; +} +var g = new Tag("a", 1); +console.log(rename(g, "b")); // boxed fast store +console.log(rename(g, 7)); // number into boxed field: generic +console.log(g.name + ":" + g.v); diff --git a/test/types/types-widen1.js b/test/types/types-widen1.js new file mode 100644 index 00000000..8a9a3be8 --- /dev/null +++ b/test/types/types-widen1.js @@ -0,0 +1,12 @@ +// reassignment widening: these do NOT diamond (documented behavior). +// `w` holds number THEN string -> the oracle reports num|str for every +// node mapped to it; `u` starts undefined -> number|undefined. Only +// exact {number} qualifies, so expect diamonds=0 — correctness must +// hold regardless (the generic ops run). +var w = 1; +console.log(w + 1); +w = "s"; +console.log(w + "!"); +var u; +u = 2; +console.log(u + 3); diff --git a/test/types/types-wrapper1.js b/test/types/types-wrapper1.js new file mode 100644 index 00000000..33657dc6 --- /dev/null +++ b/test/types/types-wrapper1.js @@ -0,0 +1,9 @@ +// external calls cross the module boundary into the wrapped export: +// numbers pass the has_tag chain into the guarded clone; the string and +// the missing-arg call fail it and run the original generic body. +// Output must be identical to the flag-off executable in every case. +import { kernel } from "./types-wrapper1/lib"; +console.log(kernel(10)); +console.log(kernel(20.5)); +console.log(kernel("3")); // guard fails -> generic path, coercing compare +console.log(kernel()); // missing arg is undefined -> guard fails diff --git a/test/types/types-wrapper1/lib.js b/test/types/types-wrapper1/lib.js new file mode 100644 index 00000000..b2badba7 --- /dev/null +++ b/test/types/types-wrapper1/lib.js @@ -0,0 +1,21 @@ +// runtime-P2 probe: the export-boundary wrapper. kernel is EXPORTED — +// its closure escapes through the non-promoted module slot, so it is +// never TRUSTED-specialized (external callers are outside the +// analysis, and maam's constant-propagation claims don't survive +// them) — but it still gets the boundary wrapper: a has_tag(number) +// guard per formal at the generic entry, dispatching to an UNTRUSTED +// f64 clone whose guarded body folds structurally from the entry +// boxes. specWrapped=1 in the stats line; behavior is identical to +// flag-off for every caller. +export function kernel(n) { + var s = 0; + var i = 0; + while (i < n) { + s = s + i * i - i / 2; + i = i + 1; + } + return s; +} +// a module-local call gives the oracle its numeric profile; it reaches +// the same wrapper guards through the generic entry +console.log(kernel(10)); diff --git a/test/types/types-wrapperfence1.js b/test/types/types-wrapperfence1.js new file mode 100644 index 00000000..9a48e4fc --- /dev/null +++ b/test/types/types-wrapperfence1.js @@ -0,0 +1,9 @@ +// external calls into the exported f reach the module-private g with +// values maam never analyzed. f(7) crosses g's constant-pruned branch +// (s becomes "s", so "s" * 2 must be NaN); a trusted rewrite of the +// g-site inside f would unbox the string unguarded and print garbage. +// Output must be identical to the flag-off executable. +import { f } from "./types-wrapperfence1/lib"; +console.log(f(7)); // NaN — the pruned branch, taken for real +console.log(f(2)); // 4 +console.log(f("2")); // "2" > 5 is false -> s = "2" -> "2" * 2 = 4 diff --git a/test/types/types-wrapperfence1/lib.js b/test/types/types-wrapperfence1/lib.js new file mode 100644 index 00000000..72fce5c5 --- /dev/null +++ b/test/types/types-wrapperfence1/lib.js @@ -0,0 +1,18 @@ +// runtime-P2 probe: the escape-taint fence. g is module-private and +// looks closed-world numeric — every context maam analyzed passes 3 — +// but one of its call sites is HOSTED in the exported f, whose +// activations can carry values the analysis never saw. Under the +// analyzed constant maam prunes g's y>5 branch, so a trusted clone of +// g reached from f with an external 7 would run `s * 2` unguarded on +// the string "s" — the pre-existing cross-module miscompile the fence +// closes. The init-time site (the toplevel g(3)) may still rewrite to +// the trusted clone; the site inside f stays generic (specFenced>=1). +function g(y) { + var s = y > 5 ? "s" : y; + return s * 2; +} +export function f(x) { + return g(x); +} +console.log(g(3)); // init-time: analyzed, rewritable +console.log(f(3)); // f's own call is analyzed too — but f escapes diff --git a/test/types/types-wrongoracle1.js b/test/types/types-wrongoracle1.js new file mode 100644 index 00000000..bca512bf --- /dev/null +++ b/test/types/types-wrongoracle1.js @@ -0,0 +1,8 @@ +// the wrong-oracle guard: lib.js's oracle typed inc's param {number} +// (its only module-local call is numeric), but cross-module linking is +// unmodeled — we call it with a string. The has_tag guard must route +// to the slow path and produce "x1": correctness never depends on the +// oracle being right. +import { inc } from "./types-wrongoracle1/lib"; +console.log(inc("x")); +console.log(inc(1.5)); diff --git a/test/types/types-wrongoracle1/lib.js b/test/types/types-wrongoracle1/lib.js new file mode 100644 index 00000000..1e776c9c --- /dev/null +++ b/test/types/types-wrongoracle1/lib.js @@ -0,0 +1,3 @@ +// module-local call sites type n as {number} -> inc's body diamonds... +export function inc(n) { return n + 1; } +console.log(inc(41)); // ...because the oracle only sees THIS call diff --git a/test/weakmap1.js b/test/weakmap1.js index a2589f96..687d9e7f 100644 --- a/test/weakmap1.js +++ b/test/weakmap1.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm // adapted from kangax's tests diff --git a/test/weakmap2.js b/test/weakmap2.js index e7c6ff3a..d7d4abd8 100644 --- a/test/weakmap2.js +++ b/test/weakmap2.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm // adapted from kangax's tests var key1 = {}; diff --git a/test/weakmap3.js b/test/weakmap3.js index d818319c..ac1d5adb 100644 --- a/test/weakmap3.js +++ b/test/weakmap3.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm // adapted from kangax's tests diff --git a/test/weakset1.js b/test/weakset1.js index da23f0ac..d417cba9 100644 --- a/test/weakset1.js +++ b/test/weakset1.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm // adapted from kangax's tests diff --git a/test/weakset2.js b/test/weakset2.js index 29bcb3a6..5976387f 100644 --- a/test/weakset2.js +++ b/test/weakset2.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm // adapted from kangax's tests diff --git a/test/weakset3.js b/test/weakset3.js index 27ef6fe1..c044e1f6 100644 --- a/test/weakset3.js +++ b/test/weakset3.js @@ -1,4 +1,4 @@ -// generator: babel-node +// generator: esm // adapted from kangax's tests diff --git a/toolchains/BUCK b/toolchains/BUCK new file mode 100644 index 00000000..5ed7c5d0 --- /dev/null +++ b/toolchains/BUCK @@ -0,0 +1,19 @@ +load("@prelude//toolchains:genrule.bzl", "system_genrule_toolchain") +load("@prelude//toolchains:cxx.bzl", "system_cxx_toolchain") +load("@prelude//toolchains:python.bzl", "system_python_bootstrap_toolchain") +load("//:llvm.bzl", "system_llvm_toolchain") + +system_genrule_toolchain( + name = "genrule", + visibility = ["PUBLIC"], +) + +system_cxx_toolchain( + name = "cxx", + visibility = ["PUBLIC"], +) + +system_python_bootstrap_toolchain( + name = "python_bootstrap", + visibility = ["PUBLIC"], +) diff --git a/toolchains/llvm.bzl b/toolchains/llvm.bzl new file mode 100644 index 00000000..de2f9b55 --- /dev/null +++ b/toolchains/llvm.bzl @@ -0,0 +1,40 @@ + +# @unsorted-dict-items +_llvm_toolchain_attrs = { + # Report unused dependencies + # "report_unused_deps": False, + # Rustc target triple to use + # https://doc.rust-lang.org/rustc/platform-support.html + # "rustc_target_triple": None, +} + +LLVMToolchainInfo = provider(fields = _llvm_toolchain_attrs.keys()) + +def _system_llvm_toolchain_impl(ctx): + return [ + DefaultInfo(), + LLVMToolchainInfo( + ) + ] + +system_llvm_toolchain = rule( + impl = _system_llvm_toolchain_impl, + attrs = { +# "allow_lints": attrs.list(attrs.string(), default = []), +# "clippy_toml": attrs.option(attrs.dep(providers = [DefaultInfo]), default = None), +# "default_edition": attrs.option(attrs.string(), default = None), +# "deny_lints": attrs.list(attrs.string(), default = []), +# "doctests": attrs.bool(default = False), +# "extern_html_root_url_prefix": attrs.option(attrs.string(), default = None), +# "pipelined": attrs.bool(default = False), +# "report_unused_deps": attrs.bool(default = False), +# "rustc_binary_flags": attrs.list(attrs.string(), default = []), +# "rustc_check_flags": attrs.list(attrs.string(), default = []), +# "rustc_flags": attrs.list(attrs.string(), default = []), +# "rustc_target_triple": attrs.string(default = _DEFAULT_TRIPLE), +# "rustc_test_flags": attrs.list(attrs.string(), default = []), +# "rustdoc_flags": attrs.list(attrs.string(), default = []), +# "warn_lints": attrs.list(attrs.string(), default = []), + }, + is_toolchain_rule = True, +) \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..68c36535 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + // Editor/IDE configuration. The build's tsc invocation lives in + // lib/buck-gen-tsjs.sh with the SAME flags — keep them in sync. + "compilerOptions": { + "strict": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "noEmitOnError": true, + "target": "es2016", + "module": "esnext", + "moduleResolution": "bundler", + "types": ["node"], + "noEmit": true + }, + "include": ["ejs-es6.ts", "lib/**/*.ts", "runtime/gen-atoms.ts"] +}