Skip to content

perf(map): repair ordered-delete indexes in place #3053

perf(map): repair ordered-delete indexes in place

perf(map): repair ordered-delete indexes in place #3053

# CI arm for the in-process LLVM backend (#7241, merged in #7301).
#
# The GC knob kill-policy (CLAUDE.md) demands every shipped mode be exercised
# by CI or deleted; this job is the exercise for `PERRY_LLVM_INPROCESS`.
# Each step asserts its subject was LIVE (liveness line, diff verdict) rather
# than merely that nothing threw — see "Four ways a gate can be unable to
# fail". NON-REQUIRED until it has run green once; promote afterwards
# (a new gate has never been green, so promoting first blocks every PR).
name: llvm-inprocess
# No trigger-level `paths:` — deliberately. A required check whose workflow
# is path-filtered at the trigger never CREATES a check run for PRs outside
# those paths, so the required context sits "waiting" forever and blocks the
# merge (the promotion trap CodeRabbit flagged on #7304). Filtering lives in
# the `changes` job instead: a job skipped by `if:` still reports a check
# run (conclusion: skipped), which branch protection accepts.
on:
pull_request:
# PR arm is OPT-IN via the `run-extended-tests` label (see the header of
# test.yml and docs/src/testing/ci-tiers.md): an unlabelled PR still gets
# a run, but every job in it is skipped, which costs no runner slot. The
# main-line arm (schedule / tags) is unchanged. `labeled` re-fires the run
# when the label lands.
types: [opened, synchronize, reopened, labeled]
# POST-MERGE ARM: staggered six-hourly sweep of `main`, NOT one run per merge.
# `push: branches: [main]` starved this gate and nine others for two days
# (#7856): ~29 jobs enqueued per merge at 58 merges/day against a ~9-job
# concurrency ceiling. The pull-request arm is unchanged -- every PR is still
# measured. ***DO NOT RESTORE `push: branches: [main]`.***
# Rationale, measurement and cost: docs/src/testing/ci-gate-scheduling.md
schedule:
- cron: "42 */6 * * *"
push:
tags: ["v*"]
workflow_dispatch:
concurrency:
# ***#7966: KEY EVERY MAIN-LINE RUN ON `github.run_id`, NOT `github.sha`.***
# The previous expression read `github.event_name == 'push' && github.sha ||
# github.ref`. That was #7205's fix and it keyed on the event being `push` --
# correct while the main-line arm WAS `push: branches: [main]`. #7856 moved the
# main-line arm to `schedule:`, which falls through to `github.ref` (constant
# `refs/heads/main`), so every scheduled run shared one group again and #7205
# came straight back. Measured 2026-08-12 on all ten scheduled gates, the same
# shape every time: oldest run `queued` holding the group, the two after it
# `cancelled` with `jobs: 0`, newest `pending`. `github.run_id` is unique per
# run, so schedule / tag-push / workflow_dispatch each get a group of their own
# and none can supersede another. PR runs keep the shared per-ref group and
# keep superseding themselves, which is still what we want.
group: llvm-inprocess-${{ github.event_name }}-${{ github.event_name == 'pull_request' && github.ref || github.run_id }}
# One group per main COMMIT, cancelling PR runs only. `cancel-in-progress:
# false` alone does not protect a `main` run: GitHub allows at most one
# PENDING run per group and cancels the previously pending one when a new run
# enters, regardless of that setting. That is #7205 — measured here as EIGHT
# consecutive `main` runs cancelled, zero executions. Keying push runs on the
# SHA gives every merged commit a group of its own.
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
# Cheap relevance filter via the PR files API (no checkout, no third-party
# action). Pushes to main always run — main executions are the gate's
# anchor and the promotion prerequisite.
changes:
# PR arm is opt-in (label `run-extended-tests`); see the `on:` block.
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-extended-tests')
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
pull-requests: read
outputs:
relevant: ${{ steps.filter.outputs.relevant }}
steps:
- id: filter
env:
GH_TOKEN: ${{ github.token }}
run: |
# Anything that is not a pull request (the scheduled `main` sweep, a
# release tag, a manual dispatch) measures unconditionally: there is no
# PR file list to filter on, and #7856's starvation is precisely what
# happens when a post-merge arm quietly declines to run. Testing for
# `= "push"` here was correct only while `push: branches: [main]` was
# the post-merge trigger; under the schedule it would fall through to
# the PR branch, dereference an empty PR number and fail the step.
if [ "${{ github.event_name }}" != "pull_request" ]; then
echo "relevant=true" >> "$GITHUB_OUTPUT"
exit 0
fi
files=$(gh api "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files" --paginate --jq '.[].filename')
if echo "$files" | grep -qE '^(crates/perry-codegen/|crates/perry/src/commands/compile/|experiments/llvm-inprocess-spike/|benchmarks/app-patterns/kernels/batch\.ts$|\.github/workflows/llvm-inprocess\.yml$)'; then
echo "relevant=true" >> "$GITHUB_OUTPUT"
else
echo "relevant=false" >> "$GITHUB_OUTPUT"
fi
native-backend:
needs: changes
if: needs.changes.outputs.relevant == 'true'
runs-on: macos-15
timeout-minutes: 90
steps:
# #7982: `fetch-depth: 0`. The "Corpus currency" step below asks git how
# many IR-affecting commits have landed since the corpora were last
# refreshed — and on the default depth-1 checkout that question has
# exactly one commit to answer from, so `git log -- <path>` finds nothing
# and the count printed was a confident **0** no matter how stale the
# files were. A diagnostic that cannot report the problem it exists to
# report is worse than no diagnostic; it was reassuring three readers
# while the corpora sat 151 commits behind.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
- name: Install LLVM 22 (pinned major — fail loudly on drift)
run: |
set -euo pipefail
brew install llvm@22 2>/dev/null || brew install llvm
PREFIX="$(brew --prefix llvm@22 2>/dev/null || brew --prefix llvm)"
# llvm-sys 221 requires major 22. If the runner's formula moves on,
# this must go red, not quietly build something else.
"$PREFIX/bin/llvm-config" --version | grep -q '^22\.'
echo "LLVM_SYS_221_PREFIX=$PREFIX" >> "$GITHUB_ENV"
- name: Build with the llvm-inprocess feature
run: |
cargo build --profile perry-dev -p perry -p perry-runtime-static \
-p perry-stdlib-static --features perry/llvm-inprocess
- name: Unit gates (528 incl. corpus construction + RS4GC pin)
run: |
set -euo pipefail
out=$(cargo test --profile perry-dev -p perry-codegen \
--features llvm-inprocess --lib 2>&1) || { echo "$out"; exit 1; }
# The corpus gates must have RUN, not skipped: a checkout missing
# the tracked .ll corpora would otherwise green vacuously.
grep -q "dialect::tests::corpus_spike ... ok" <<<"$out"
grep -q "dialect::tests::corpus_batch_kernel ... ok" <<<"$out"
grep -q "dialect::tests::corpus_exception_handling ... ok" <<<"$out"
grep -q "inprocess::tests::rs4gc_schedules_in_process ... ok" <<<"$out"
# The tracked `.ll` corpora above are a SNAPSHOT of what the compiler
# emitted when they were last refreshed (#7302/#7307/#7310, 2026-08-03).
# `corpus_spike ... ok` therefore proves the dialect reader can build
# THAT IR — not the IR this commit emits. When the end-to-end arm below
# goes red while these stay green, that gap is the first thing to check,
# so print it rather than leaving the next reader to rediscover it.
# #7982: the FORM census is the gate (it runs in `lint` and fails when a
# form the reader has a branch for is absent from the corpora). This step
# keeps the age number, which is the other half of the picture and the
# one a human reads when the end-to-end arm goes red while the unit gates
# stay green. It needs `fetch-depth: 0` above to be anything but zero.
- name: Corpus currency (diagnostic; the gate is `lint`'s form census)
if: ${{ !cancelled() }}
run: |
set -uo pipefail
python3 scripts/check_llvm_corpus_currency.py
newest=$(git log -1 --format=%ct -- experiments/llvm-inprocess-spike/*.ll)
if [ -z "${newest}" ]; then
echo "::error::corpus currency could not read git history — is this a"\
"shallow checkout again? (#7982: depth 1 made this print 0 forever)"
exit 1
fi
behind=$(git log --oneline --since="@${newest}" -- crates/perry-codegen/src \
crates/perry-hir/src crates/perry-transform/src | wc -l | tr -d ' ')
echo "tracked .ll corpora last refreshed: $(git log -1 --format='%h %ad' \
--date=short -- experiments/llvm-inprocess-spike/*.ll)"
echo "IR-affecting commits since then (codegen+hir+transform): ${behind}"
{
echo "### llvm-inprocess corpus currency"
echo ""
echo "- corpora refreshed: \`$(git log -1 --format='%h %ad' --date=short \
-- experiments/llvm-inprocess-spike/*.ll)\`"
echo "- IR-affecting commits since: **${behind}**"
echo ""
echo "The unit corpus gates assert the reader handles that snapshot."
echo "Only the end-to-end smoke below exercises the IR this commit emits."
} >> "$GITHUB_STEP_SUMMARY"
- name: Native-mode smoke — liveness, behavior parity, object-byte verdicts
run: |
# NOTE: deliberately NOT `set -e`. Every check below reports what it
# was doing and dumps the captured output before exiting. The previous
# version used bare `grep -q` / `cmp` under `set -euo pipefail`, so the
# 2026-08-11 `main` failures ended at "Generating code..." with a naked
# `exit 1` and no diagnostic at all — three runs, untriageable (#7971).
# The compiler's own message was fine and named the offending IR line;
# it went to a captured file that nothing ever printed. That is #7982.
# The compilers' stderr carries the liveness banner, so it is captured
# to a file; that file is what went unread. It is now always dumped on
# failure, and the failing COMMAND is named.
set -uo pipefail
export PERRY_RUNTIME_DIR="$PWD/target/perry-dev"
export PERRY_NO_AUTO_OPTIMIZE=1
BIN=target/perry-dev/perry
SRC=experiments/llvm-inprocess-spike/spike.ts
EH=test-files/test_gap_7302_invoke_eh_paths.ts
W=/tmp/inproc; mkdir -p "$W"
dump() {
for f in "$@"; do
[ -s "$f" ] || continue
echo "--- $f (last 80 lines) ---"
tail -80 "$f"
done
}
# run <label> -- <command...> ; stdout/stderr captured per label
run() {
local label="$1"; shift; [ "$1" = "--" ] && shift
echo "==> ${label}: $*"
# Capture the status BEFORE any other command runs. `if ! cmd; then
# rc=$?` would record the NEGATED status (always 0) — the failing
# exit code is the one thing this whole step exists to report.
"$@" > "${W}/${label}.out" 2> "${W}/${label}.err"
local rc=$?
if [ "${rc}" -ne 0 ]; then
echo "::error::${label} FAILED (exit ${rc}): $*"
dump "${W}/${label}.out" "${W}/${label}.err"
exit 1
fi
}
# assert_grep <pattern> <file> <why this matters>
assert_grep() {
if ! grep -q "$1" "$2"; then
echo "::error::liveness assert failed — expected /$1/ in $2. $3"
dump "$2"
exit 1
fi
echo " ok: /$1/ present in $(basename "$2")"
}
# assert_same <a> <b> <why>
assert_same() {
if ! cmp -s "$1" "$2"; then
echo "::error::behavior parity failed: $3"
echo "--- diff $1 vs $2 ---"
diff -u "$1" "$2" | head -60 || true
exit 1
fi
echo " ok: $(basename "$1") == $(basename "$2")"
}
echo "::group::spike.ts — textual baseline"
run spike_text -- "$BIN" "$SRC" -o "${W}/spike_text"
run spike_text_exec -- "${W}/spike_text"
cp "${W}/spike_text_exec.out" "${W}/text.out"
echo "::endgroup::"
echo "::group::spike.ts — PERRY_LLVM_INPROCESS=native"
run spike_native -- env PERRY_LLVM_INPROCESS=native "$BIN" "$SRC" -o "${W}/spike_native"
assert_grep "in-process LLVM backend active" "${W}/spike_native.err" \
"The native path must announce itself; without the banner this arm would pass while silently serving the textual backend."
run spike_native_exec -- "${W}/spike_native"
assert_same "${W}/text.out" "${W}/spike_native_exec.out" \
"the natively-constructed module behaves differently from the textual one"
echo "::endgroup::"
echo "::group::spike.ts — PERRY_LLVM_INPROCESS=diff"
run spike_diff -- env PERRY_LLVM_INPROCESS=diff "$BIN" "$SRC" -o "${W}/spike_diff"
assert_grep "ir-diff. OK" "${W}/spike_diff.err" \
"The object-byte verdict is the whole point of diff mode; no verdict means nothing was compared."
echo "::endgroup::"
echo "::group::batch.ts — diff mode across 3 codegen units"
run batch_diff -- env PERRY_LLVM_INPROCESS=diff PERRY_CODEGEN_UNITS=3 \
"$BIN" benchmarks/app-patterns/kernels/batch.ts -o "${W}/batch_diff"
assert_grep "ir-diff. OK.*3 units" "${W}/batch_diff.err" \
"The multi-unit split must be exercised; a single-unit verdict here would be a narrower test wearing the same name."
echo "::endgroup::"
# #7302: exception handling. try/catch lowers to invoke/landingpad
# with a personality on the define, so a reader that cannot build
# those forms silently loses every try-containing module to the
# textual path — which is exactly how this arm went red once the
# EH migration landed. Assert the EH program takes the native path
# AND behaves identically.
echo "::group::EH program — textual, native, diff"
run eh_text -- "$BIN" "$EH" -o "${W}/eh_text"
run eh_text_exec -- "${W}/eh_text"
run eh_native -- env PERRY_LLVM_INPROCESS=native "$BIN" "$EH" -o "${W}/eh_native"
assert_grep "in-process LLVM backend active" "${W}/eh_native.err" \
"A try-containing module must take the native path, not fall back to text (#7302)."
run eh_native_exec -- "${W}/eh_native"
assert_same "${W}/eh_text_exec.out" "${W}/eh_native_exec.out" \
"exception-handling behavior differs between the textual and native backends"
run eh_diff -- env PERRY_LLVM_INPROCESS=diff "$BIN" "$EH" -o "${W}/eh_diff"
assert_grep "ir-diff. OK" "${W}/eh_diff.err" \
"The EH module must reach a byte verdict, not be skipped by the differ."
echo "::endgroup::"
echo "native-mode smoke: all arms exercised and green"
# ── Fan-in verdict ─────────────────────────────────────────────────────────
#
# #7971: WITHOUT this job the workflow reported `success` on a PR where
# `changes=success, native-backend=skipped` — i.e. green while executing
# nothing. Three sampled PR "successes" (31505530279, 31499833415,
# 31476724152) were all of that shape. That is CLAUDE.md's fourth way a gate
# cannot fail, and the most dangerous one, because the job is genuinely green.
#
# A path filter is a COST control, not a verdict. This job separates the two:
# it re-states in the log and the summary whether the backend was actually
# exercised, so "llvm-inprocess ✓" can no longer be read as "the in-process
# backend passed" when nothing ran.
#
# It can fail, in two directions that matter:
# * `native-backend` failed or was cancelled -> red.
# * `native-backend` was SKIPPED on a non-PR event -> red. The scheduled
# sweep, a release tag and a manual dispatch all set `relevant=true`
# unconditionally; if one of them ever skips, the post-merge anchor has
# silently stopped anchoring, which is exactly how #7856 starved this
# gate for eight days without anyone noticing.
llvm-inprocess-complete:
needs: [changes, native-backend]
if: (always()) && (github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-extended-tests'))
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Verdict
env:
CHANGES: ${{ needs.changes.result }}
BACKEND: ${{ needs.native-backend.result }}
RELEVANT: ${{ needs.changes.outputs.relevant }}
EVENT: ${{ github.event_name }}
run: |
set -uo pipefail
echo "changes=${CHANGES} relevant=${RELEVANT} native-backend=${BACKEND} event=${EVENT}"
if [ "${CHANGES}" != "success" ]; then
echo "::error::the relevance filter itself did not succeed (${CHANGES});"\
"no statement can be made about the in-process backend."
exit 1
fi
case "${BACKEND}" in
success)
echo "EXERCISED: the in-process LLVM backend ran and passed."
echo "✅ **EXERCISED** — in-process LLVM backend ran and passed." \
>> "$GITHUB_STEP_SUMMARY"
;;
skipped)
if [ "${EVENT}" != "pull_request" ]; then
echo "::error::native-backend was SKIPPED on a ${EVENT} run."\
"Every non-PR event sets relevant=true unconditionally, so this"\
"means the post-merge anchor has stopped anchoring (#7856/#7971)."
exit 1
fi
echo "NOT EXERCISED: no IR-affecting path changed, so the backend did not run."
echo "This run asserts NOTHING about the in-process LLVM backend."
{
echo "⚠️ **NOT EXERCISED** — no IR-affecting path changed in this PR."
echo ""
echo "The in-process LLVM backend did **not** run. This green result"
echo "is a statement about relevance, not about the backend."
echo "The post-merge sweep of \`main\` is what anchors this gate."
} >> "$GITHUB_STEP_SUMMARY"
;;
*)
echo "::error::native-backend concluded '${BACKEND}'."
echo "❌ **${BACKEND}** — in-process LLVM backend did not pass." \
>> "$GITHUB_STEP_SUMMARY"
exit 1
;;
esac