Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions changelog.d/7320-loop-safepoint-purity-knob.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
### Fix `loop_safepoint_purity`, red on `main` since the moving-GC work

Six of the seven tests in `crates/perry-codegen/tests/loop_safepoint_purity.rs`
were failing on `main`. Every assertion in that file is about *which* loops keep
their back-edge `js_gc_loop_safepoint()` poll, but the poll is gated behind
`PERRY_GC_MOVING_LOOP_POLLS`, which defaults off — so the emitter returned
before the purity analysis ran and the six "sabotage" cases (a call in the body,
an object literal, string concatenation, a module-global accumulator, coercible
bound/accumulator) saw no poll for a reason unrelated to what they test. The one
test asserting a *pure* numeric loop drops its poll passed for the same wrong
reason.

The suite now enables the knob it asserts on, before the first codegen call —
`moving_safepoint_polls_enabled` caches in a `OnceLock`.

Worth noting how it stayed red: integration suites under `crates/*/tests/` do
not run per-PR (nightly/tag only), which CLAUDE.md already calls out as a way a
regression "can land green and sit red for days".

This makes the suite exercise the poll-ON path. The **default** path — polls
off, which is what ships — remains untested, and `PERRY_GC_MOVING_LOOP_POLLS`
is a GC knob with no CI arm covering either state. That is a kill-policy
question for the owner, not something this change decides.
18 changes: 18 additions & 0 deletions crates/perry-codegen/tests/loop_safepoint_purity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,14 +125,32 @@ fn module_with_init(name: &str, init: Vec<Stmt>) -> Module {
}
}

/// The back-edge poll is gated behind `PERRY_GC_MOVING_LOOP_POLLS`, which
/// defaults OFF. Every assertion in this file is about WHICH loops keep the
/// poll, so with the knob unset the emitter returns before the purity analysis
/// runs and six of these tests fail for a reason that has nothing to do with
/// what they test.
///
/// That is how they went red: the knob arrived with the moving-GC work and
/// nothing here turned it on, while integration suites under `crates/*/tests/`
/// do not run per-PR, so nobody saw it. `moving_safepoint_polls_enabled`
/// caches in a `OnceLock`, so this must run before the first codegen call —
/// every helper that produces IR goes through here.
Comment on lines +134 to +138

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)(loop_safepoint_purity\.rs|7320-loop-safepoint-purity-knob\.md|CLAUDE\.md|.*\.ya?ml|.*\.toml|.*\.sh)$' || true

echo
echo "== targeted docs =="
for f in crates/perry-codegen/tests/loop_safepoint_purity.rs changelog.d/7320-loop-safepoint-purity-knob.md CLAUDE.md; do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    nl -ba "$f" | sed -n '1,220p'
  else
    echo "--- $f not found ---"
  fi
done

echo
echo "== workflow/action references =="
rg -n "e2e(scoped)?|crate.*test|crates/[^/]+/tests|integration|per-PR|per PR|changed|changed files|cargo test|CLAUDE.md|nightly|tag" \
  .github . 2>/dev/null | \
  rg '\.(ya?ml|toml|rs|md|sh)$|e2e|integration|changed|CLAUDE|nightly|tag|Per-PR|per PR' || true

Repository: PerryTS/perry

Length of output: 143


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== top-level relevant files =="
git ls-files CLAUDE.md .github | sed -n '1,200p'

echo
echo "== targeted docs =="
for f in crates/perry-codegen/tests/loop_safepoint_purity.rs changelog.d/7320-loop-safepoint-purity-knob.md CLAUDE.md; do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    nl -ba "$f" | sed -n '1,220p'
  else
    echo "--- $f not found ---"
  fi
done

echo
echo "== CI workflow candidates =="
git ls-files '.github/workflows/*.yml' '.github/workflows/*.yaml' | sed -n '1,200p'

echo
echo "== targeted CI text snippets =="
rg -n "e2e(scoped)?|changed|per[ -]PR|per[ -]PR[s]?|nightly|tag|CLAUDE.md|integration|tests" .github crates/perry-codegen/tests/loop_safepoint_purity.rs changelog.d/7320-loop-safepoint-purity-knob.md CLAUDE.md 2>/dev/null | sed -n '1,240p'

Repository: PerryTS/perry

Length of output: 1383


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== read files with cat -n =="
for f in crates/perry-codegen/tests/loop_safepoint_purity.rs changelog.d/7320-loop-safepoint-purity-knob.md CLAUDE.md; do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    cat -n "$f" | sed -n '1,220p'
  else
    echo "--- $f not found ---"
  fi
done

echo
echo "== CI workflow candidates size and names =="
while IFS= read -r f; do
  wc -l "$f"
done < <(git ls-files '.github/workflows/*.yml' '.github/workflows/*.yaml')

echo
echo "== target CI workflow snippets =="
for f in .github/workflows/e2e*.yml .github/workflows/e2e*.yaml .github/workflows/test.yml .github/workflows/node-suite-guard.yml .github/workflows/cache-warm.yml CLAUDE.md crates/perry-codegen/tests/loop_safepoint_purity.rs changelog.d/7320-loop-safepoint-purity-knob.md; do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    grep -n -i -C 3 "e2e\|scoped\|changed\|per-PR\|per PR\|nightly\|tag\|CLAUDE.md\|integration\|tests" "$f" || true
  fi
done

Repository: PerryTS/perry

Length of output: 39474


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== workflow files =="
git ls-files '.github/workflows/*.yml' '.github/workflows/*.yaml' | while IFS= read -r f; do
  printf '%s\n' "$f"
done

echo
echo "== targeted CI references =="
for f in $(git ls-files '.github/workflows/*.yml' '.github/workflows/*.yaml' | grep -E 'e2e|node-suite|test|cache-warm|container|coverage' || true); do
  echo "--- $f ---"
  grep -n -i -C 4 "e2e\|scoped\|changed\|per[ -]PR\|per[ -]PRs\|nightly\|tag\|CLAUDE.md\|crates/\*/tests\|crate.*test\|cargo test" "$f" || true
done

echo
echo "== current docs around CI coverage claims =="
{
  echo "--- crates/perry-codegen/tests/loop_safepoint_purity.rs:133:138 ---"
  sed -n '133,138p' crates/perry-codegen/tests/loop_safepoint_purity.rs
  echo "--- changelog.d/7320-loop-safepoint-purity-knob.md:16:18 ---"
  sed -n '16,18p' changelog.d/7320-loop-safepoint-purity-knob.md
}

Repository: PerryTS/perry

Length of output: 143


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== workflow files =="
git ls-files '.github/workflows/*.yml' '.github/workflows/*.yaml' > /tmp/workflows.txt
cat /tmp/workflows.txt

echo
echo "== targeted CI references =="
while IFS= read -r f; do
  echo "--- $f ---"
  grep -n -i -C 4 "e2e\|scoped\|changed\|per[ -]PR\|per[ -]PRs\|nightly\|tag\|CLAUDE.md\|crates/\*/tests\|crate.*test\|cargo test" "$ f" || true
done < /tmp/workflows.txt

echo
echo "== current docs around CI coverage claims =="
sed -n '133,138p' crates/perry-codegen/tests/loop_safepoint_purity.rs
sed -n '16,18p' changelog.d/7320-loop-safepoint-purity-knob.md

Repository: PerryTS/perry

Length of output: 143


Correct the duplicated per-PR CI coverage claim.

Current in-repo guidance says integration-like test workflows include a PR-only scoped job, so these docs still say the suites are nightly/tag only. Update both crates/perry-codegen/tests/loop_safepoint_purity.rs#L133-138 and changelog.d/7320-loop-safepoint-purity-knob.md#L16-18 to remove that duplicated incorrect claim or to specify the exact historical CI state/exclusion that hid the regression.

📍 Affects 2 files
  • crates/perry-codegen/tests/loop_safepoint_purity.rs#L134-L138 (this comment)
  • changelog.d/7320-loop-safepoint-purity-knob.md#L16-L18
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-codegen/tests/loop_safepoint_purity.rs` around lines 134 - 138,
Remove or correct the inaccurate CI coverage claim in the documentation around
moving_safepoint_polls_enabled: update
crates/perry-codegen/tests/loop_safepoint_purity.rs lines 134-138 and
changelog.d/7320-loop-safepoint-purity-knob.md lines 16-18 to either omit the
duplicated statement or describe the exact historical CI exclusion that allowed
the regression.

Source: Learnings

fn enable_back_edge_polls() {
// Safe on edition 2021, and every test in this binary wants the same value.
std::env::set_var("PERRY_GC_MOVING_LOOP_POLLS", "1");
}
Comment on lines +139 to +142

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n '^\s*edition\s*=' --glob 'Cargo.toml' .
cargo check -p perry --profile perry-dev
cargo check -p perry-codegen --tests --profile perry-dev

Repository: PerryTS/perry

Length of output: 143


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== Cargo edition pins =="
rg -n '^\s*edition\s*=' --glob 'Cargo.toml' . || true

echo
echo "== Target file outline/size =="
if [ -f crates/perry-codegen/tests/loop_safepoint_purity.rs ]; then
  wc -l crates/perry-codegen/tests/loop_safepoint_purity.rs
  sed -n '1,220p' crates/perry-codegen/tests/loop_safepoint_purity.rs
else
  fd -a 'loop_safepoint_purity.rs' .
fi

echo
echo "== perry-codegen Cargo metadata =="
fd -a 'Cargo.toml' crates/perry-codegen . | while read -r f; do
  echo "--- $f"
  sed -n '1,180p' "$f"
done || true

echo
echo "== std::env::set_var usages in tests/codegen =="
rg -n 'std::env::set_var|set_var\(' crates/perry-codegen crates/perry || true

echo
echo "== loop_polls variable usages =="
rg -n 'PERRY_GC_MOVING_LOOP_POLLS|loop_polls|back_edge_polls|OnceLock|Once' crates/perry-codegen crates/perry || true

Repository: PerryTS/perry

Length of output: 50370


🌐 Web query:

Rust edition 2024 std::env::set_var unsafe process-wide environment mutable global documentation

💡 Result:

In the Rust 2024 Edition, std::env::set_var and std::env::remove_var have been marked as unsafe functions [1][2]. This change was implemented because mutating the process environment is inherently thread-unsafe on many platforms [1][3]. The standard library cannot guarantee that other threads—including those within C libraries or other parts of the Rust runtime—are not concurrently reading or writing the environment, which can lead to undefined behavior or memory safety issues [3][4]. Key details regarding this change include: * Reason for Unsafety: On most operating systems, the process environment is not thread-safe. Functions like setenv and unsetenv can invalidate pointers returned by getenv, and concurrent access to these variables is a common source of data races [1][3][5]. Because there is no stable, cross-platform way to synchronize all possible readers and writers of the environment (including external FFI code), these functions are fundamentally unsafe in multi-threaded contexts [3][4]. * Scope of Safety: * It is considered sound to call std::env::set_var or std::env::remove_var in a single-threaded program [1][3]. * On Windows, these functions are generally sound to call even in multi-threaded programs [3][6]. * On other operating systems (such as Unix-like systems), the only sound option in a multi-threaded program is to avoid using set_var and remove_var entirely [3]. * Migration: To assist with the transition to the 2024 Edition, Rust provides a deprecated_safe_2024 lint, which is part of the rust-2024-compatibility lint group. Running cargo fix --edition will automatically wrap existing calls to these functions in an unsafe block, allowing the code to compile under the new edition requirements [1][2]. * Recommendation: Developers are encouraged to avoid mutating the environment and instead use alternative mechanisms, such as passing environment variables explicitly to child processes using std::process::Command::env [3].

Citations:


Serialize the process-wide environment setup.

Multiple tests in this binary can call ir_for_* concurrently, so concurrent std::env::set_var writes can race. Use a test-local Once so only one write happens and the result is shared. The project is pinned to Rust 2021, so this is safe to keep without an unsafe block. Also run cargo check -p perry and cargo check -p perry-codegen --tests --profile perry-dev with this change.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-codegen/tests/loop_safepoint_purity.rs` around lines 139 - 142,
Update enable_back_edge_polls to guard the process-wide
PERRY_GC_MOVING_LOOP_POLLS set_var call with a test-local std::sync::Once,
ensuring concurrent ir_for_* tests perform the write only once; keep the Rust
2021 implementation without an unsafe block. Validate with cargo check -p perry
and cargo check -p perry-codegen --tests --profile perry-dev.

Source: Coding guidelines


fn ir_for(name: &str, init: Vec<Stmt>) -> String {
enable_back_edge_polls();
String::from_utf8(compile_module(&module_with_init(name, init), entry_opts()).unwrap())
.expect("LLVM IR should be UTF-8")
}

/// Same, but `exported` names are exported module variables — which is what
/// promotes a module-level `let` to a `@perry_global_*` slot.
fn ir_for_with_exported_vars(name: &str, init: Vec<Stmt>, exported: &[&str]) -> String {
enable_back_edge_polls();
let mut m = module_with_init(name, init);
m.exported_objects = exported.iter().map(|s| s.to_string()).collect();
String::from_utf8(compile_module(&m, entry_opts()).unwrap()).expect("LLVM IR should be UTF-8")
Expand Down
Loading