Skip to content

fix: recover main's red CI gates + perry check false pass - #6891

Open
proggeramlug wants to merge 3 commits into
mainfrom
fix/linux-verification-sweep
Open

fix: recover main's red CI gates + perry check false pass#6891
proggeramlug wants to merge 3 commits into
mainfrom
fix/linux-verification-sweep

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Found while verifying a fresh Linux checkout end-to-end (build → gap suite → cargo test → lint gates). Everything here except the perry check bug was already red on main.

The one real user-facing bug

perry check returned exit 0 and "All checks passed!" on code that does not parse.

$ perry check <dir-with-one-unparseable-file>
Checking 1 file(s)...
All checks passed! - 0 file(s) checked      # exit 0

check.rs printed the parse error only under -v, then continued without touching all_diagnostics, so error_count() stayed 0 and the success branch ran. perry compile rejected the same file correctly. Now recorded as a P001 diagnostic, so the text summary, the JSON success field and the exit code agree. There was no test coverage for perry check exit codes anywhere in crates/perry/tests/.

Red CI gates on main

Gate Before Fix
check_file_size.sh object/mod.rs 2039, readline.rs 2066 (cap 2000) extracted object/spill.rs (→1709) and readline/pump.rs (→1796)
cargo fmt --check 1 diff in collect_modules.rs blank line so rustfmt stops indenting the comment block to col 40
addr_class_inventory.py baseline stale recorded the fixed accessors.rs site + readline path move

Both splits move code verbatim; only visibility widened so existing use super::* sites keep resolving.

Three stale test assertions

Each verified behaviourally against Node 26.5.0 before touching — none was a real defect:

Two of these live in crates/*/tests/*.rs, which don't run per-PR (nightly/tag only) — the #5960 failure mode. The randomBytes one is a --lib unit test that does run per-PR.

Harness: 7 false regressions

Under PERRY_NO_AUTO_OPTIMIZE the default test-files/ suite linked the prebuilt full stdlib, which has no external-* pumps — so every events/http/net/fetch gap test failed and was reported as an untriaged NEW gap failure with no hint the run mode caused it. All 7 pass with auto-optimize (verified individually). node-suite already compensated; the default suite now does too.

This matters locally because a cold auto-optimize run isn't feasible on one machine — it mints a ThinLTO variant per feature combo (measured: 12 variants / 71 GB after 30 of 411 tests, extrapolating to ~1 TB).

Docs

libclang (bindgen → libsqlite3-sys) and rustc ≥ 1.94 (sqlx 0.9.0's MSRV; nothing pins a toolchain) were undocumented build prerequisites — both hard-fail a fresh checkout with confusing errors. installation.md also stated Linux needs "clang ≥ 15 for codegen" while every per-distro command below it installed no clang. Node now points at .node-version rather than the stale "22".

Verification

  • Gap suite 386/395, every remaining failure triaged in known_failures.json
  • cargo test green: perry-parser, -hir, -transform, -codegen, -dispatch, -api-manifest, perry-runtime --lib (1468), perry-stdlib --lib, perry --bins (734)
  • Both clippy scopes (product + host-compatible): 0 deny-level
  • All five lint gates green
  • 4 e2e programs (core / async / node-stdlib / threads) byte-identical to Node 26.5.0; threading suite 6/6; memory-stability RSS 32/32 across 4 GC modes

Not fixed — needs a GC owner

run_memory_stability_tests.sh exits 1 on 8 telemetry-attribution assertions (e.g. expected BarriersInactive, got NotAttempted). These are pre-existing and not safety failures: the safety assertions in the same tests pass (the candidate is neither moved nor forwarded), both real correctness canaries (evacuation verifier surfaces, copying minor rewrites) pass, and all 32 RSS runs pass including force-evac+verify with PERRY_GC_VERIFY_EVACUATION=1.

Lead worth checking: gc/mod.rs:120 returns to a full mark-sweep via copied_minor_promotion_handoff_due before the copying fast path evaluates eligibility, which would leave copying_nursery.fallback_reason at its NotAttempted default and never reach the BarriersInactive gate. I deliberately did not touch this — relaxing the assertion could mask a real ordering regression, and changing the policy needs the owner's intent.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Improved readline input handling, including ANSI escape sequences, keypress events, pause/resume behavior, and stream shutdown.
    • Custom readline interfaces now clean up reliably after closing.
  • Bug Fixes
    • perry check now correctly reports parse failures in text and JSON results and returns a failing exit code.
    • Fixed callback timing for crypto.randomBytes.
    • Improved parity-test reliability and object storage behavior.
  • Documentation
    • Updated build and installation requirements, including Rust, Clang, libclang, and Node.js guidance.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 659298d4-55dc-42a8-b93b-f4fdb066b881

📥 Commits

Reviewing files that changed from the base of the PR and between 1d075c9 and 81bb06d.

📒 Files selected for processing (15)
  • CONTRIBUTING.md
  • changelog.d/6891-linux-verification-sweep.md
  • crates/perry-codegen/tests/native_proof_regressions.rs
  • crates/perry-hir/tests/c262_parity.rs
  • crates/perry-runtime/src/object/mod.rs
  • crates/perry-runtime/src/object/spill.rs
  • crates/perry-stdlib/src/crypto/random.rs
  • crates/perry-stdlib/src/readline/mod.rs
  • crates/perry-stdlib/src/readline/pump.rs
  • crates/perry/src/commands/check.rs
  • crates/perry/src/commands/compile/collect_modules.rs
  • docs/src/contributing/building.md
  • docs/src/getting-started/installation.md
  • run_parity_tests.sh
  • scripts/addr_class_ratchet_baseline.txt
🚧 Files skipped from review as they are similar to previous changes (13)
  • run_parity_tests.sh
  • crates/perry-stdlib/src/crypto/random.rs
  • crates/perry-hir/tests/c262_parity.rs
  • crates/perry/src/commands/compile/collect_modules.rs
  • scripts/addr_class_ratchet_baseline.txt
  • docs/src/getting-started/installation.md
  • CONTRIBUTING.md
  • crates/perry/src/commands/check.rs
  • crates/perry-codegen/tests/native_proof_regressions.rs
  • crates/perry-stdlib/src/readline/pump.rs
  • docs/src/contributing/building.md
  • crates/perry-stdlib/src/readline/mod.rs
  • crates/perry-runtime/src/object/spill.rs

📝 Walkthrough

Walkthrough

This PR extracts object spill storage and readline pumping into dedicated modules, fixes parse-error reporting, updates readline behavior and GC handling, adjusts parity and regression tests, and documents Linux toolchain prerequisites.

Changes

Object overflow storage

Layer / File(s) Summary
Spill allocation and access
crates/perry-runtime/src/object/mod.rs, crates/perry-runtime/src/object/spill.rs
Adds gated object-owned spill storage, owner validation, spill reads and writes, growth handling, slot barriers, and module re-exports.
Legacy overflow caching
crates/perry-runtime/src/object/spill.rs
Preserves legacy overflow fallback, learned inline-width tracking, cached vectors, resizing, and GC metadata updates.

Readline pump extraction

Layer / File(s) Summary
Keypress decoding and pending-event dispatch
crates/perry-stdlib/src/readline/pump.rs, crates/perry-stdlib/src/readline/mod.rs
Adds keypress object construction, ANSI escape reassembly, queued line and byte dispatch, readable/EOF handling, and event-loop activity checks.
Readline lifecycle and GC safety
crates/perry-stdlib/src/readline/mod.rs
Hardens custom-interface cleanup, callback dispatch, GC rooting and scanning, pause/resume behavior, output writes, stdin cleanup, and platform lock recovery.
Readline pump validation
crates/perry-stdlib/src/readline/mod.rs
Updates test state and reader setup and covers split escape sequences, bare escape flushing, keypress recording, and readable notifications.

Verification and prerequisites

Layer / File(s) Summary
Diagnostics and parity verification
crates/perry/src/commands/check.rs, run_parity_tests.sh, changelog.d/...
Records parse failures as P001 diagnostics and enables external pump features for no-auto-optimize parity runs.
Regression test updates
crates/perry-codegen/tests/native_proof_regressions.rs, crates/perry-hir/tests/c262_parity.rs, crates/perry-stdlib/src/crypto/random.rs, crates/perry/src/commands/compile/collect_modules.rs
Updates IR extraction, logical-assignment expectations, deferred callback timing, and formatting.
Toolchain documentation and metadata
CONTRIBUTING.md, docs/src/contributing/building.md, docs/src/getting-started/installation.md, scripts/addr_class_ratchet_baseline.txt
Documents Rust, libclang, clang, and Node requirements, updates Linux installation commands, and refreshes ratchet entries.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant stdin_reader
  participant readline_pump
  participant callbacks
  stdin_reader->>readline_pump: queue bytes or lines
  readline_pump->>readline_pump: reassemble escape sequences
  readline_pump->>callbacks: dispatch data, keypress, line, or close
Loading

Possibly related issues

Possibly related PRs

Suggested labels: bug

Suggested reviewers: andrewtdiz, thehypnoo

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is specific and matches the main change: fixing red CI gates and the perry check false pass.
Description check ✅ Passed The description is detailed and covers summary, changes, verification, and docs, but it lacks the template's explicit section headings.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/linux-verification-sweep

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

proggeramlug pushed a commit that referenced this pull request Jul 27, 2026
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 11

🧹 Nitpick comments (7)
crates/perry-runtime/src/object/spill.rs (1)

122-142: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Move the learned-inline-fields hook off the hot write path.

note_learned_inline_fields runs on every spill_set call (including the in-capacity fast path at Lines 128-138), which forces a thread_local access exactly on the path this module was built to keep TLS-free (per the doc at Lines 52-58 calling out TLS probes as "the hot leaves of round-robin overflow writes"). Since a new high-water field index can only be set the first time it exceeds current capacity, gating the call to spill_set_slow preserves identical learned-maximum semantics while removing the TLS touch from every subsequent hot-path write.

⚡ Proposed fix
 pub(crate) fn spill_set(obj_ptr: usize, field_index: usize, vbits: u64) {
     unsafe {
         let obj = obj_ptr as *mut ObjectHeader;
-        // Learn the class's true width so FUTURE instances allocate it
-        // inline (same hook as the legacy path).
-        note_learned_inline_fields((*obj).class_id, (field_index as u32).saturating_add(1));
         // Hot path: meta and buffer already exist with capacity — the
         // in-range barriered store cannot allocate or move anything, so no
         // handle scope is needed. This is every write after the first to a
         // given width (e.g. round-robin updates across an object array).
         let meta = (*obj).meta;
 fn spill_set_slow(obj_ptr: usize, field_index: usize, vbits: u64) {
     unsafe {
         let obj = obj_ptr as *mut ObjectHeader;
+        // Learn the class's true width so FUTURE instances allocate it
+        // inline (same hook as the legacy path). Only needs to run here:
+        // any field_index that sets a new high-water mark must first
+        // exceed current capacity, which always routes through this path.
+        note_learned_inline_fields((*obj).class_id, (field_index as u32).saturating_add(1));
         // Root the owner: meta/buffer allocation below can trigger a moving
         // minor GC. Reload through the handle after every allocation.
         let scope = crate::gc::RuntimeHandleScope::new();
🤖 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-runtime/src/object/spill.rs` around lines 122 - 142, Move the
note_learned_inline_fields call out of spill_set’s entry path and into
spill_set_slow, after confirming the write requires slow-path growth. Keep the
existing high-water update semantics for first-time capacity expansion while
ensuring in-capacity writes return without any TLS access.
crates/perry-stdlib/src/readline/mod.rs (4)

1002-1002: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

SAVED.lock().unwrap() panics on a poisoned mutex.

Every other lock site in this file recovers (unwrap_or_else(|p| p.into_inner()) / if let Ok). If a panic ever unwinds while SAVED is held, setRawMode aborts the process instead of degrading. Use unwrap_or_else(|p| p.into_inner()) here too.

Also applies to: 1030-1030, 1091-1091, 1113-1113

🤖 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-stdlib/src/readline/mod.rs` at line 1002, Update every SAVED
mutex lock in setRawMode and the additional affected sites to recover from
poisoning with unwrap_or_else(|p| p.into_inner()) instead of unwrap(). Preserve
the existing lock usage and behavior otherwise.

523-542: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Interface slots are never reclaimed.

NEXT_READLINE_HANDLE increments forever and close_custom_interface only sets closed = true, so the READLINE_INTERFACES vector grows monotonically and every closed interface's state (plus its rooted input/output/callbacks) is kept alive by the GC scanner for the process lifetime. A long-running program that creates one interface per child process leaks steadily. Consider setting the slot to None on close and reusing free indices.

🤖 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-stdlib/src/readline/mod.rs` around lines 523 - 542, The readline
interface lifecycle must reclaim closed custom-interface slots instead of
retaining their state indefinitely. Update close_custom_interface to remove the
closed interface from READLINE_INTERFACES, and update allocate_interface to
reuse available None slots before incrementing NEXT_READLINE_HANDLE; preserve
the fixed STDIN_READLINE_HANDLE behavior.

1825-1886: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Tests share process-global state with a real reader thread; reset() can't undo that.

READER_STARTED is sticky and several tests (raw_mode_toggle_flips_atomic, stdin_pause_resume_gates_data_dispatch via js_readline_stdin_resume) actually spawn the background thread, which then blocks on the runner's stdin and flips EOF_REACHED at arbitrary points — the comment at Line 1973 already documents having to work around it. Consider gating ensure_reader_started behind a test-only flag so the suite never spawns a real stdin reader.

🤖 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-stdlib/src/readline/mod.rs` around lines 1825 - 1886, Prevent
tests from spawning the real stdin reader by adding a test-only gate checked by
ensure_reader_started and disabling reader startup while the test suite runs.
Update reset() or the test setup to enable the mocked/test path instead,
preserving existing state-reset behavior and avoiding background changes to
EOF_REACHED.

906-956: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Per-byte blocking reads and an unbounded pending queue.

reader.read(&mut byte) with a 1-byte buffer issues a syscall per keystroke/byte; for piped input (cat big.txt | app) that is one syscall and one Vec push per byte. Combined with PENDING_DATA/PENDING_LINES having no cap, a paused or destroyed-but-still-blocked consumer lets the queue grow to the size of the whole input stream. Consider a buffered read (e.g. BufReader + read into a 4 KiB buffer, splitting on \n in cooked mode) and a bounded queue with backpressure.

🤖 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-stdlib/src/readline/mod.rs` around lines 906 - 956, Update the
stdin reader thread around PENDING_DATA and PENDING_LINES to use a buffered
reader and a multi-byte buffer instead of calling reader.read with a one-byte
buffer; preserve raw-mode byte ordering and cooked-mode newline/CRLF behavior
while processing each buffered chunk. Bound both pending queues and apply
backpressure when they are full, including avoiding unbounded line_buf growth,
so paused or destroyed consumers cannot retain input indefinitely.
crates/perry-stdlib/src/readline/pump.rs (2)

22-48: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

if cond { JSValue::bool(true) } else { JSValue::bool(false) }JSValue::bool(cond).

♻️ Proposed refactor
-    js_object_set_field(
-        obj,
-        1,
-        if ctrl {
-            JSValue::bool(true)
-        } else {
-            JSValue::bool(false)
-        },
-    );
-    js_object_set_field(
-        obj,
-        2,
-        if shift {
-            JSValue::bool(true)
-        } else {
-            JSValue::bool(false)
-        },
-    );
-    js_object_set_field(
-        obj,
-        3,
-        if meta {
-            JSValue::bool(true)
-        } else {
-            JSValue::bool(false)
-        },
-    );
+    js_object_set_field(obj, 1, JSValue::bool(ctrl));
+    js_object_set_field(obj, 2, JSValue::bool(shift));
+    js_object_set_field(obj, 3, JSValue::bool(meta));
🤖 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-stdlib/src/readline/pump.rs` around lines 22 - 48, In the object
field assignments, simplify the boolean conversions in the relevant function by
passing ctrl, shift, and meta directly to JSValue::bool instead of using
redundant if/else expressions. Preserve the existing field indices and values.

165-189: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Re-locking and re-parsing inside the per-chunk loop.

DATA_CALLBACKS and KEYPRESS_CALLBACKS are cloned under lock once per chunk, and parse_keypress (which allocates two Strings) runs once per callback rather than once per chunk. Hoist both out of the loop.

♻️ Proposed refactor
+    let data_callbacks = DATA_CALLBACKS.lock().map(|v| v.clone()).unwrap_or_default();
+    let keypress_callbacks = KEYPRESS_CALLBACKS
+        .lock()
+        .map(|v| v.clone())
+        .unwrap_or_default();
     for chunk in chunks {
-        // 'data' callback receives the raw bytes as a string.
-        let data_callbacks = DATA_CALLBACKS.lock().map(|v| v.clone()).unwrap_or_default();
-        for cb_i64 in data_callbacks {
+        for cb_i64 in &data_callbacks {
             let arg = stdin_chunk_value(&chunk);
-            let closure = cb_i64 as *const ClosureHeader;
-            js_closure_call1(closure, arg);
+            js_closure_call1(*cb_i64 as *const ClosureHeader, arg);
             fired += 1;
         }
-        // 'keypress' callback receives (sequence_string, key_object).
-        let keypress_callbacks = KEYPRESS_CALLBACKS
-            .lock()
-            .map(|v| v.clone())
-            .unwrap_or_default();
-        for cb_i64 in keypress_callbacks {
-            if let Some((name, ctrl, shift, meta, seq)) = parse_keypress(&chunk) {
+        if let Some((name, ctrl, shift, meta, seq)) = parse_keypress(&chunk) {
+            for cb_i64 in &keypress_callbacks {
                 let seq_str = js_string_from_bytes(seq.as_ptr(), seq.len() as u32);
                 let arg1 = f64::from_bits(JSValue::string_ptr(seq_str).bits());
                 let arg2 = build_keypress_object(&name, ctrl, shift, meta, &seq);
-                let closure = cb_i64 as *const ClosureHeader;
-                js_closure_call2(closure, arg1, arg2);
+                js_closure_call2(*cb_i64 as *const ClosureHeader, arg1, arg2);
                 fired += 1;
             }
         }
     }
🤖 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-stdlib/src/readline/pump.rs` around lines 165 - 189, Update the
pump callback dispatch flow to clone DATA_CALLBACKS and KEYPRESS_CALLBACKS once
before iterating chunks, and parse each chunk’s keypress data once before
invoking callbacks. Reuse the per-chunk parsed keypress result for all
KEYPRESS_CALLBACKS while preserving the existing data and keypress callback
arguments and fired count.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@crates/perry-stdlib/src/readline/mod.rs`:
- Around line 1269-1287: Update js_readline_pause and js_readline_resume to
branch on the interface state’s uses_custom_stream flag: preserve forwarding to
node_stream pause/resume for custom streams, and for stdin-backed interfaces set
or clear the shared STDIN_PAUSED state so line delivery stops and resumes.
- Around line 1247-1267: Update js_readline_close on the stdin path to clear
both LINE_CALLBACK and QUESTION_CALLBACK when setting EOF_REACHED. Preserve the
existing custom-stream handling and close callback behavior, ensuring subsequent
has_line_callbacks and js_readline_process_pending calls cannot emit late line
events after closure.
- Around line 772-796: The async iterator construction in
crates/perry-stdlib/src/readline/mod.rs:772-796 must root obj with
RuntimeHandleScope and reload it after every allocating call before field writes
and iter_val creation; preserve the existing NaN-boxed value handling and
symbol-property setup. In crates/perry-stdlib/src/readline/pump.rs:16-21, move
the name_str and seq_str allocations before js_object_alloc_with_shape, or root
and reload obj across those string allocations.
- Around line 811-846: In attach_custom_input, keep the data and close closure
allocations rooted across subsequent js_closure_alloc and boxed_str calls, using
the runtime’s established rooting mechanism before constructing data_value and
close_value. Ensure both closure values remain valid until registration through
js_node_stream_method_on or js_native_call_value completes.
- Around line 1381-1399: Register the GC scanner before any root-bearing state
is stored: update js_readline_stdin_on to call ensure_gc_scanner_registered() at
entry, and update js_readline_create_interface plus js_readline_iterator so
state.input/state.output are covered even when question or on is unused. Apply
the changes at crates/perry-stdlib/src/readline/mod.rs lines 1381-1399 and
1155-1164, preserving the existing NaN-boxed value representation and registered
module-global root invariants.
- Around line 1515-1520: Update destroy() to also lock and clear
READABLE_CALLBACKS alongside DATA_CALLBACKS and KEYPRESS_CALLBACKS, ensuring no
readable callbacks remain active after destruction.
- Around line 1313-1320: Update js_readline_write to send the converted text
through call_write_value using state.output, matching the output behavior in
js_readline_prompt, while preserving the existing cursor_cols update and return
value.
- Around line 1533-1805: Remove build_keypress_object, parse_keypress,
js_readline_process_pending, and js_readline_has_active from readline/mod.rs,
then declare the pump module and re-export its public symbols from mod.rs.
Ensure the existing readline::* export path continues exposing these APIs while
keeping their single implementations in pump.rs and bringing mod.rs below the
2,000-line limit.
- Around line 635-645: Update the non-async dispatch path around
with_interface_mut and fire_line_or_question: borrow the interface only long
enough to extract or clone the callbacks/state required for delivery, then
release that borrow before invoking any callbacks. Preserve the existing line
ordering and async_iter behavior, while ensuring fire_line_or_question and any
nested JS execution run after the READLINE_INTERFACES borrow ends.

In `@crates/perry-stdlib/src/readline/pump.rs`:
- Around line 54-58: Update js_readline_process_pending and the related
parse_keypress flow to reassemble ESC-prefixed ANSI sequences across separate
byte chunks before decoding, so \x1b, [, A produces one up keypress. Remove or
correct the parse_keypress documentation’s reference to a nonexistent
pending_escape accumulator, and add an end-to-end test covering the three-chunk
input and single up result.
- Around line 155-163: Update js_readline_process_pending so READABLE_CALLBACKS
are invoked only when newly read chunks are available, or once when EOF is
reached, rather than on every event-loop tick. Ensure paused or destroyed stdin
states do not dispatch readable callbacks, and prevent the fired count from
keeping the loop active when no eligible readable event occurred.

---

Nitpick comments:
In `@crates/perry-runtime/src/object/spill.rs`:
- Around line 122-142: Move the note_learned_inline_fields call out of
spill_set’s entry path and into spill_set_slow, after confirming the write
requires slow-path growth. Keep the existing high-water update semantics for
first-time capacity expansion while ensuring in-capacity writes return without
any TLS access.

In `@crates/perry-stdlib/src/readline/mod.rs`:
- Line 1002: Update every SAVED mutex lock in setRawMode and the additional
affected sites to recover from poisoning with unwrap_or_else(|p| p.into_inner())
instead of unwrap(). Preserve the existing lock usage and behavior otherwise.
- Around line 523-542: The readline interface lifecycle must reclaim closed
custom-interface slots instead of retaining their state indefinitely. Update
close_custom_interface to remove the closed interface from READLINE_INTERFACES,
and update allocate_interface to reuse available None slots before incrementing
NEXT_READLINE_HANDLE; preserve the fixed STDIN_READLINE_HANDLE behavior.
- Around line 1825-1886: Prevent tests from spawning the real stdin reader by
adding a test-only gate checked by ensure_reader_started and disabling reader
startup while the test suite runs. Update reset() or the test setup to enable
the mocked/test path instead, preserving existing state-reset behavior and
avoiding background changes to EOF_REACHED.
- Around line 906-956: Update the stdin reader thread around PENDING_DATA and
PENDING_LINES to use a buffered reader and a multi-byte buffer instead of
calling reader.read with a one-byte buffer; preserve raw-mode byte ordering and
cooked-mode newline/CRLF behavior while processing each buffered chunk. Bound
both pending queues and apply backpressure when they are full, including
avoiding unbounded line_buf growth, so paused or destroyed consumers cannot
retain input indefinitely.

In `@crates/perry-stdlib/src/readline/pump.rs`:
- Around line 22-48: In the object field assignments, simplify the boolean
conversions in the relevant function by passing ctrl, shift, and meta directly
to JSValue::bool instead of using redundant if/else expressions. Preserve the
existing field indices and values.
- Around line 165-189: Update the pump callback dispatch flow to clone
DATA_CALLBACKS and KEYPRESS_CALLBACKS once before iterating chunks, and parse
each chunk’s keypress data once before invoking callbacks. Reuse the per-chunk
parsed keypress result for all KEYPRESS_CALLBACKS while preserving the existing
data and keypress callback arguments and fired count.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 15ea0ba2-9fe8-4823-9414-c279ddf12977

📥 Commits

Reviewing files that changed from the base of the PR and between 5b4e70e and 9ab8608.

📒 Files selected for processing (4)
  • changelog.d/6891-linux-verification-sweep.md
  • crates/perry-runtime/src/object/spill.rs
  • crates/perry-stdlib/src/readline/mod.rs
  • crates/perry-stdlib/src/readline/pump.rs

Comment thread crates/perry-stdlib/src/readline/pump.rs Outdated
Comment thread crates/perry-stdlib/src/readline/pump.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 11

🧹 Nitpick comments (7)
crates/perry-runtime/src/object/spill.rs (1)

122-142: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Move the learned-inline-fields hook off the hot write path.

note_learned_inline_fields runs on every spill_set call (including the in-capacity fast path at Lines 128-138), which forces a thread_local access exactly on the path this module was built to keep TLS-free (per the doc at Lines 52-58 calling out TLS probes as "the hot leaves of round-robin overflow writes"). Since a new high-water field index can only be set the first time it exceeds current capacity, gating the call to spill_set_slow preserves identical learned-maximum semantics while removing the TLS touch from every subsequent hot-path write.

⚡ Proposed fix
 pub(crate) fn spill_set(obj_ptr: usize, field_index: usize, vbits: u64) {
     unsafe {
         let obj = obj_ptr as *mut ObjectHeader;
-        // Learn the class's true width so FUTURE instances allocate it
-        // inline (same hook as the legacy path).
-        note_learned_inline_fields((*obj).class_id, (field_index as u32).saturating_add(1));
         // Hot path: meta and buffer already exist with capacity — the
         // in-range barriered store cannot allocate or move anything, so no
         // handle scope is needed. This is every write after the first to a
         // given width (e.g. round-robin updates across an object array).
         let meta = (*obj).meta;
 fn spill_set_slow(obj_ptr: usize, field_index: usize, vbits: u64) {
     unsafe {
         let obj = obj_ptr as *mut ObjectHeader;
+        // Learn the class's true width so FUTURE instances allocate it
+        // inline (same hook as the legacy path). Only needs to run here:
+        // any field_index that sets a new high-water mark must first
+        // exceed current capacity, which always routes through this path.
+        note_learned_inline_fields((*obj).class_id, (field_index as u32).saturating_add(1));
         // Root the owner: meta/buffer allocation below can trigger a moving
         // minor GC. Reload through the handle after every allocation.
         let scope = crate::gc::RuntimeHandleScope::new();
🤖 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-runtime/src/object/spill.rs` around lines 122 - 142, Move the
note_learned_inline_fields call out of spill_set’s entry path and into
spill_set_slow, after confirming the write requires slow-path growth. Keep the
existing high-water update semantics for first-time capacity expansion while
ensuring in-capacity writes return without any TLS access.
crates/perry-stdlib/src/readline/mod.rs (4)

1002-1002: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

SAVED.lock().unwrap() panics on a poisoned mutex.

Every other lock site in this file recovers (unwrap_or_else(|p| p.into_inner()) / if let Ok). If a panic ever unwinds while SAVED is held, setRawMode aborts the process instead of degrading. Use unwrap_or_else(|p| p.into_inner()) here too.

Also applies to: 1030-1030, 1091-1091, 1113-1113

🤖 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-stdlib/src/readline/mod.rs` at line 1002, Update every SAVED
mutex lock in setRawMode and the additional affected sites to recover from
poisoning with unwrap_or_else(|p| p.into_inner()) instead of unwrap(). Preserve
the existing lock usage and behavior otherwise.

523-542: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Interface slots are never reclaimed.

NEXT_READLINE_HANDLE increments forever and close_custom_interface only sets closed = true, so the READLINE_INTERFACES vector grows monotonically and every closed interface's state (plus its rooted input/output/callbacks) is kept alive by the GC scanner for the process lifetime. A long-running program that creates one interface per child process leaks steadily. Consider setting the slot to None on close and reusing free indices.

🤖 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-stdlib/src/readline/mod.rs` around lines 523 - 542, The readline
interface lifecycle must reclaim closed custom-interface slots instead of
retaining their state indefinitely. Update close_custom_interface to remove the
closed interface from READLINE_INTERFACES, and update allocate_interface to
reuse available None slots before incrementing NEXT_READLINE_HANDLE; preserve
the fixed STDIN_READLINE_HANDLE behavior.

1825-1886: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Tests share process-global state with a real reader thread; reset() can't undo that.

READER_STARTED is sticky and several tests (raw_mode_toggle_flips_atomic, stdin_pause_resume_gates_data_dispatch via js_readline_stdin_resume) actually spawn the background thread, which then blocks on the runner's stdin and flips EOF_REACHED at arbitrary points — the comment at Line 1973 already documents having to work around it. Consider gating ensure_reader_started behind a test-only flag so the suite never spawns a real stdin reader.

🤖 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-stdlib/src/readline/mod.rs` around lines 1825 - 1886, Prevent
tests from spawning the real stdin reader by adding a test-only gate checked by
ensure_reader_started and disabling reader startup while the test suite runs.
Update reset() or the test setup to enable the mocked/test path instead,
preserving existing state-reset behavior and avoiding background changes to
EOF_REACHED.

906-956: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Per-byte blocking reads and an unbounded pending queue.

reader.read(&mut byte) with a 1-byte buffer issues a syscall per keystroke/byte; for piped input (cat big.txt | app) that is one syscall and one Vec push per byte. Combined with PENDING_DATA/PENDING_LINES having no cap, a paused or destroyed-but-still-blocked consumer lets the queue grow to the size of the whole input stream. Consider a buffered read (e.g. BufReader + read into a 4 KiB buffer, splitting on \n in cooked mode) and a bounded queue with backpressure.

🤖 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-stdlib/src/readline/mod.rs` around lines 906 - 956, Update the
stdin reader thread around PENDING_DATA and PENDING_LINES to use a buffered
reader and a multi-byte buffer instead of calling reader.read with a one-byte
buffer; preserve raw-mode byte ordering and cooked-mode newline/CRLF behavior
while processing each buffered chunk. Bound both pending queues and apply
backpressure when they are full, including avoiding unbounded line_buf growth,
so paused or destroyed consumers cannot retain input indefinitely.
crates/perry-stdlib/src/readline/pump.rs (2)

22-48: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

if cond { JSValue::bool(true) } else { JSValue::bool(false) }JSValue::bool(cond).

♻️ Proposed refactor
-    js_object_set_field(
-        obj,
-        1,
-        if ctrl {
-            JSValue::bool(true)
-        } else {
-            JSValue::bool(false)
-        },
-    );
-    js_object_set_field(
-        obj,
-        2,
-        if shift {
-            JSValue::bool(true)
-        } else {
-            JSValue::bool(false)
-        },
-    );
-    js_object_set_field(
-        obj,
-        3,
-        if meta {
-            JSValue::bool(true)
-        } else {
-            JSValue::bool(false)
-        },
-    );
+    js_object_set_field(obj, 1, JSValue::bool(ctrl));
+    js_object_set_field(obj, 2, JSValue::bool(shift));
+    js_object_set_field(obj, 3, JSValue::bool(meta));
🤖 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-stdlib/src/readline/pump.rs` around lines 22 - 48, In the object
field assignments, simplify the boolean conversions in the relevant function by
passing ctrl, shift, and meta directly to JSValue::bool instead of using
redundant if/else expressions. Preserve the existing field indices and values.

165-189: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Re-locking and re-parsing inside the per-chunk loop.

DATA_CALLBACKS and KEYPRESS_CALLBACKS are cloned under lock once per chunk, and parse_keypress (which allocates two Strings) runs once per callback rather than once per chunk. Hoist both out of the loop.

♻️ Proposed refactor
+    let data_callbacks = DATA_CALLBACKS.lock().map(|v| v.clone()).unwrap_or_default();
+    let keypress_callbacks = KEYPRESS_CALLBACKS
+        .lock()
+        .map(|v| v.clone())
+        .unwrap_or_default();
     for chunk in chunks {
-        // 'data' callback receives the raw bytes as a string.
-        let data_callbacks = DATA_CALLBACKS.lock().map(|v| v.clone()).unwrap_or_default();
-        for cb_i64 in data_callbacks {
+        for cb_i64 in &data_callbacks {
             let arg = stdin_chunk_value(&chunk);
-            let closure = cb_i64 as *const ClosureHeader;
-            js_closure_call1(closure, arg);
+            js_closure_call1(*cb_i64 as *const ClosureHeader, arg);
             fired += 1;
         }
-        // 'keypress' callback receives (sequence_string, key_object).
-        let keypress_callbacks = KEYPRESS_CALLBACKS
-            .lock()
-            .map(|v| v.clone())
-            .unwrap_or_default();
-        for cb_i64 in keypress_callbacks {
-            if let Some((name, ctrl, shift, meta, seq)) = parse_keypress(&chunk) {
+        if let Some((name, ctrl, shift, meta, seq)) = parse_keypress(&chunk) {
+            for cb_i64 in &keypress_callbacks {
                 let seq_str = js_string_from_bytes(seq.as_ptr(), seq.len() as u32);
                 let arg1 = f64::from_bits(JSValue::string_ptr(seq_str).bits());
                 let arg2 = build_keypress_object(&name, ctrl, shift, meta, &seq);
-                let closure = cb_i64 as *const ClosureHeader;
-                js_closure_call2(closure, arg1, arg2);
+                js_closure_call2(*cb_i64 as *const ClosureHeader, arg1, arg2);
                 fired += 1;
             }
         }
     }
🤖 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-stdlib/src/readline/pump.rs` around lines 165 - 189, Update the
pump callback dispatch flow to clone DATA_CALLBACKS and KEYPRESS_CALLBACKS once
before iterating chunks, and parse each chunk’s keypress data once before
invoking callbacks. Reuse the per-chunk parsed keypress result for all
KEYPRESS_CALLBACKS while preserving the existing data and keypress callback
arguments and fired count.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@crates/perry-stdlib/src/readline/mod.rs`:
- Around line 1269-1287: Update js_readline_pause and js_readline_resume to
branch on the interface state’s uses_custom_stream flag: preserve forwarding to
node_stream pause/resume for custom streams, and for stdin-backed interfaces set
or clear the shared STDIN_PAUSED state so line delivery stops and resumes.
- Around line 1247-1267: Update js_readline_close on the stdin path to clear
both LINE_CALLBACK and QUESTION_CALLBACK when setting EOF_REACHED. Preserve the
existing custom-stream handling and close callback behavior, ensuring subsequent
has_line_callbacks and js_readline_process_pending calls cannot emit late line
events after closure.
- Around line 772-796: The async iterator construction in
crates/perry-stdlib/src/readline/mod.rs:772-796 must root obj with
RuntimeHandleScope and reload it after every allocating call before field writes
and iter_val creation; preserve the existing NaN-boxed value handling and
symbol-property setup. In crates/perry-stdlib/src/readline/pump.rs:16-21, move
the name_str and seq_str allocations before js_object_alloc_with_shape, or root
and reload obj across those string allocations.
- Around line 811-846: In attach_custom_input, keep the data and close closure
allocations rooted across subsequent js_closure_alloc and boxed_str calls, using
the runtime’s established rooting mechanism before constructing data_value and
close_value. Ensure both closure values remain valid until registration through
js_node_stream_method_on or js_native_call_value completes.
- Around line 1381-1399: Register the GC scanner before any root-bearing state
is stored: update js_readline_stdin_on to call ensure_gc_scanner_registered() at
entry, and update js_readline_create_interface plus js_readline_iterator so
state.input/state.output are covered even when question or on is unused. Apply
the changes at crates/perry-stdlib/src/readline/mod.rs lines 1381-1399 and
1155-1164, preserving the existing NaN-boxed value representation and registered
module-global root invariants.
- Around line 1515-1520: Update destroy() to also lock and clear
READABLE_CALLBACKS alongside DATA_CALLBACKS and KEYPRESS_CALLBACKS, ensuring no
readable callbacks remain active after destruction.
- Around line 1313-1320: Update js_readline_write to send the converted text
through call_write_value using state.output, matching the output behavior in
js_readline_prompt, while preserving the existing cursor_cols update and return
value.
- Around line 1533-1805: Remove build_keypress_object, parse_keypress,
js_readline_process_pending, and js_readline_has_active from readline/mod.rs,
then declare the pump module and re-export its public symbols from mod.rs.
Ensure the existing readline::* export path continues exposing these APIs while
keeping their single implementations in pump.rs and bringing mod.rs below the
2,000-line limit.
- Around line 635-645: Update the non-async dispatch path around
with_interface_mut and fire_line_or_question: borrow the interface only long
enough to extract or clone the callbacks/state required for delivery, then
release that borrow before invoking any callbacks. Preserve the existing line
ordering and async_iter behavior, while ensuring fire_line_or_question and any
nested JS execution run after the READLINE_INTERFACES borrow ends.

In `@crates/perry-stdlib/src/readline/pump.rs`:
- Around line 54-58: Update js_readline_process_pending and the related
parse_keypress flow to reassemble ESC-prefixed ANSI sequences across separate
byte chunks before decoding, so \x1b, [, A produces one up keypress. Remove or
correct the parse_keypress documentation’s reference to a nonexistent
pending_escape accumulator, and add an end-to-end test covering the three-chunk
input and single up result.
- Around line 155-163: Update js_readline_process_pending so READABLE_CALLBACKS
are invoked only when newly read chunks are available, or once when EOF is
reached, rather than on every event-loop tick. Ensure paused or destroyed stdin
states do not dispatch readable callbacks, and prevent the fired count from
keeping the loop active when no eligible readable event occurred.

---

Nitpick comments:
In `@crates/perry-runtime/src/object/spill.rs`:
- Around line 122-142: Move the note_learned_inline_fields call out of
spill_set’s entry path and into spill_set_slow, after confirming the write
requires slow-path growth. Keep the existing high-water update semantics for
first-time capacity expansion while ensuring in-capacity writes return without
any TLS access.

In `@crates/perry-stdlib/src/readline/mod.rs`:
- Line 1002: Update every SAVED mutex lock in setRawMode and the additional
affected sites to recover from poisoning with unwrap_or_else(|p| p.into_inner())
instead of unwrap(). Preserve the existing lock usage and behavior otherwise.
- Around line 523-542: The readline interface lifecycle must reclaim closed
custom-interface slots instead of retaining their state indefinitely. Update
close_custom_interface to remove the closed interface from READLINE_INTERFACES,
and update allocate_interface to reuse available None slots before incrementing
NEXT_READLINE_HANDLE; preserve the fixed STDIN_READLINE_HANDLE behavior.
- Around line 1825-1886: Prevent tests from spawning the real stdin reader by
adding a test-only gate checked by ensure_reader_started and disabling reader
startup while the test suite runs. Update reset() or the test setup to enable
the mocked/test path instead, preserving existing state-reset behavior and
avoiding background changes to EOF_REACHED.
- Around line 906-956: Update the stdin reader thread around PENDING_DATA and
PENDING_LINES to use a buffered reader and a multi-byte buffer instead of
calling reader.read with a one-byte buffer; preserve raw-mode byte ordering and
cooked-mode newline/CRLF behavior while processing each buffered chunk. Bound
both pending queues and apply backpressure when they are full, including
avoiding unbounded line_buf growth, so paused or destroyed consumers cannot
retain input indefinitely.

In `@crates/perry-stdlib/src/readline/pump.rs`:
- Around line 22-48: In the object field assignments, simplify the boolean
conversions in the relevant function by passing ctrl, shift, and meta directly
to JSValue::bool instead of using redundant if/else expressions. Preserve the
existing field indices and values.
- Around line 165-189: Update the pump callback dispatch flow to clone
DATA_CALLBACKS and KEYPRESS_CALLBACKS once before iterating chunks, and parse
each chunk’s keypress data once before invoking callbacks. Reuse the per-chunk
parsed keypress result for all KEYPRESS_CALLBACKS while preserving the existing
data and keypress callback arguments and fired count.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 15ea0ba2-9fe8-4823-9414-c279ddf12977

📥 Commits

Reviewing files that changed from the base of the PR and between 5b4e70e and 9ab8608.

📒 Files selected for processing (4)
  • changelog.d/6891-linux-verification-sweep.md
  • crates/perry-runtime/src/object/spill.rs
  • crates/perry-stdlib/src/readline/mod.rs
  • crates/perry-stdlib/src/readline/pump.rs
🛑 Comments failed to post (9)
crates/perry-stdlib/src/readline/mod.rs (9)

635-645: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Line dispatch runs JS while the READLINE_INTERFACES borrow is held.

The comment at Lines 616-618 says delivery must happen outside the borrow, but the non-async branch calls fire_line_or_question (→ js_closure_call1) inside with_interface_mut. Any callback that re-enters the interface (rl.close(), rl.setPrompt(), rl.iterator(), another append_custom_input from a nested emit) hits RefCell borrow_mut twice → panic. Additionally, scan_readline_roots_mut uses try_borrow_mut and will silently skip the interface slots while this borrow is live, so a moving GC during callback allocation leaves input/output/callback slots pointing at stale from-space addresses.

Extract the callbacks first, then invoke.

🐛 Proposed fix: dispatch outside the borrow
     } else {
-        with_interface_mut(handle, |state| {
-            for line in lines {
-                fire_line_or_question(state, line);
-            }
-        });
+        for line in lines {
+            let cb = with_interface_mut(handle, |state| {
+                state.line.clear();
+                state
+                    .question_callback
+                    .take()
+                    .or(state.line_callback)
+            })
+            .flatten();
+            if let Some(cb_i64) = cb {
+                js_closure_call1(cb_i64 as *const ClosureHeader, callback_arg(&line));
+            }
+        }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    if async_iter {
        for line in lines {
            deliver_async_iter_line(handle, line);
        }
    } else {
        for line in lines {
            let cb = with_interface_mut(handle, |state| {
                state.line.clear();
                state
                    .question_callback
                    .take()
                    .or(state.line_callback)
            })
            .flatten();
            if let Some(cb_i64) = cb {
                js_closure_call1(cb_i64 as *const ClosureHeader, callback_arg(&line));
            }
        }
    }
🤖 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-stdlib/src/readline/mod.rs` around lines 635 - 645, Update the
non-async dispatch path around with_interface_mut and fire_line_or_question:
borrow the interface only long enough to extract or clone the callbacks/state
required for delivery, then release that borrow before invoking any callbacks.
Preserve the existing line ordering and async_iter behavior, while ensuring
fire_line_or_question and any nested JS execution run after the
READLINE_INTERFACES borrow ends.

772-796: 🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift

Raw object pointers are held live across allocating runtime calls without a RuntimeHandleScope. In both places a pointer returned by js_object_alloc_with_shape is kept in a local and then used after further allocations (closures, strings, symbols), any of which can trigger a moving minor GC and evacuate the object — the later js_object_set_field writes then land in from-space and the returned value is stale. crates/perry-runtime/src/object/spill.rs Lines 152-155 shows the required pattern: root the pointer and reload it through the handle after every allocation.

  • crates/perry-stdlib/src/readline/mod.rs#L772-L796: root obj in a handle scope and reload before each js_object_set_field / before computing iter_val, since js_closure_alloc, well_known_symbol and js_object_set_symbol_property all allocate.
  • crates/perry-stdlib/src/readline/pump.rs#L16-L21: allocate name_str and seq_str before js_object_alloc_with_shape, or root obj across the two js_string_from_bytes calls.

As per coding guidelines: "Preserve the NaN-boxed JavaScript value representation and GC-rooting invariants when modifying runtime code, including correct pointer/string/BigInt tags and registered module-global roots."

📍 Affects 2 files
  • crates/perry-stdlib/src/readline/mod.rs#L772-L796 (this comment)
  • crates/perry-stdlib/src/readline/pump.rs#L16-L21
🤖 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-stdlib/src/readline/mod.rs` around lines 772 - 796, The async
iterator construction in crates/perry-stdlib/src/readline/mod.rs:772-796 must
root obj with RuntimeHandleScope and reload it after every allocating call
before field writes and iter_val creation; preserve the existing NaN-boxed value
handling and symbol-property setup. In
crates/perry-stdlib/src/readline/pump.rs:16-21, move the name_str and seq_str
allocations before js_object_alloc_with_shape, or root and reload obj across
those string allocations.

Source: Coding guidelines


811-846: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

data_value/close_value are unrooted across subsequent allocations.

data is allocated at Line 815 and its NaN-boxed value captured at 817, then js_closure_alloc (818) and three boxed_str calls (821-823) can each move the young closure before it is finally handed to js_node_stream_method_on / js_native_call_value. Root the closures (or allocate the event strings first, immediately before use) so a minor GC between allocation and registration can't install a stale listener pointer.

🤖 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-stdlib/src/readline/mod.rs` around lines 811 - 846, In
attach_custom_input, keep the data and close closure allocations rooted across
subsequent js_closure_alloc and boxed_str calls, using the runtime’s established
rooting mechanism before constructing data_value and close_value. Ensure both
closure values remain valid until registration through js_node_stream_method_on
or js_native_call_value completes.

1247-1267: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

js_readline_close on the stdin path doesn't clear LINE_CALLBACK/QUESTION_CALLBACK.

After rl.close() the loop is expected to quiesce, but has_line_callbacks still reports true, and js_readline_process_pending will still deliver a late line to the 'line' handler even though the interface is closed. Node stops emitting 'line' after close. Clear both cells alongside setting EOF_REACHED.

🤖 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-stdlib/src/readline/mod.rs` around lines 1247 - 1267, Update
js_readline_close on the stdin path to clear both LINE_CALLBACK and
QUESTION_CALLBACK when setting EOF_REACHED. Preserve the existing custom-stream
handling and close callback behavior, ensuring subsequent has_line_callbacks and
js_readline_process_pending calls cannot emit late line events after closure.

1269-1287: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

rl.pause()/rl.resume() are no-ops for the stdin-backed interface.

Both only forward to node_stream when state.input is a stream pointer. For the default createInterface({input: process.stdin}) singleton, input is not a node stream, so pausing an interface reading stdin does not stop 'line' delivery. Consider gating on uses_custom_stream and flipping STDIN_PAUSED for the stdin path.

🤖 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-stdlib/src/readline/mod.rs` around lines 1269 - 1287, Update
js_readline_pause and js_readline_resume to branch on the interface state’s
uses_custom_stream flag: preserve forwarding to node_stream pause/resume for
custom streams, and for stdin-backed interfaces set or clear the shared
STDIN_PAUSED state so line delivery stops and resumes.

1313-1320: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

rl.write(chunk) never writes anything.

It only bumps cursor_cols. Node's Interface.write() writes the chunk to the interface's output stream. Route the text through call_write_value(state.output, &text) as js_readline_prompt does.

🐛 Proposed fix
 pub extern "C" fn js_readline_write(handle: i64, chunk: f64) -> f64 {
     let text = value_to_string(chunk);
     with_interface_mut(handle, |state| {
         state.cursor_cols = state.cursor_cols.max(text.chars().count() as i32);
+        call_write_value(state.output, &text);
     });
     undefined()
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

#[no_mangle]
pub extern "C" fn js_readline_write(handle: i64, chunk: f64) -> f64 {
    let text = value_to_string(chunk);
    with_interface_mut(handle, |state| {
        state.cursor_cols = state.cursor_cols.max(text.chars().count() as i32);
        call_write_value(state.output, &text);
    });
    undefined()
}
🤖 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-stdlib/src/readline/mod.rs` around lines 1313 - 1320, Update
js_readline_write to send the converted text through call_write_value using
state.output, matching the output behavior in js_readline_prompt, while
preserving the existing cursor_cols update and return value.

1381-1399: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Two public entry points install GC-visible closure/value slots without ever registering the root scanner. ensure_gc_scanner_registered() is called from stdin_on_op, js_readline_question and js_readline_on, but not from the externs codegen actually lowers process.stdin.on(...) and readline.createInterface(...) to. A program that uses only those paths stores raw closure pointers and NaN-boxed stream values that the moving collector never rewrites — the exact stale-pointer dispatch the scanner comment at Lines 178-195 was written to prevent.

  • crates/perry-stdlib/src/readline/mod.rs#L1381-L1399: call ensure_gc_scanner_registered() at the top of js_readline_stdin_on, before pushing into DATA_CALLBACKS/KEYPRESS_CALLBACKS/READABLE_CALLBACKS.
  • crates/perry-stdlib/src/readline/mod.rs#L1155-L1164: call ensure_gc_scanner_registered() in js_readline_create_interface (and in js_readline_iterator) so state.input/state.output are rewritten across a moving GC even when question/on are never used.

As per coding guidelines: "Preserve the NaN-boxed JavaScript value representation and GC-rooting invariants when modifying runtime code, including correct pointer/string/BigInt tags and registered module-global roots."

📍 Affects 1 file
  • crates/perry-stdlib/src/readline/mod.rs#L1381-L1399 (this comment)
  • crates/perry-stdlib/src/readline/mod.rs#L1155-L1164
🤖 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-stdlib/src/readline/mod.rs` around lines 1381 - 1399, Register
the GC scanner before any root-bearing state is stored: update
js_readline_stdin_on to call ensure_gc_scanner_registered() at entry, and update
js_readline_create_interface plus js_readline_iterator so
state.input/state.output are covered even when question or on is unused. Apply
the changes at crates/perry-stdlib/src/readline/mod.rs lines 1381-1399 and
1155-1164, preserving the existing NaN-boxed value representation and registered
module-global root invariants.

Source: Coding guidelines


1515-1520: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

destroy() leaves READABLE_CALLBACKS populated.

DATA_CALLBACKS and KEYPRESS_CALLBACKS are cleared but the readable list is not, so js_readline_has_active still sees has_stdin_callbacks == true and the pump still iterates and invokes those closures (see pump.rs Lines 155-163, which run regardless of STDIN_DESTROYED). Clear it here too.

🐛 Proposed fix
     if let Ok(mut v) = KEYPRESS_CALLBACKS.lock() {
         v.clear();
     }
+    if let Ok(mut v) = READABLE_CALLBACKS.lock() {
+        v.clear();
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    if let Ok(mut v) = DATA_CALLBACKS.lock() {
        v.clear();
    }
    if let Ok(mut v) = KEYPRESS_CALLBACKS.lock() {
        v.clear();
    }
    if let Ok(mut v) = READABLE_CALLBACKS.lock() {
        v.clear();
    }
🤖 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-stdlib/src/readline/mod.rs` around lines 1515 - 1520, Update
destroy() to also lock and clear READABLE_CALLBACKS alongside DATA_CALLBACKS and
KEYPRESS_CALLBACKS, ensuring no readable callbacks remain active after
destruction.

1533-1805: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm pump.rs is wired as a submodule and that the symbols are defined twice.
fd -t f 'mod.rs' crates/perry-stdlib/src/readline
rg -n '^\s*(pub\s+)?mod\s+pump\s*;' crates/perry-stdlib/src
echo '--- duplicate no_mangle externs ---'
rg -n -B1 'pub extern "C" fn (js_readline_process_pending|js_readline_has_active)' crates/perry-stdlib/src
echo '--- duplicate helpers ---'
rg -n 'fn (build_keypress_object|parse_keypress)\b' crates/perry-stdlib/src
echo '--- file sizes vs CI cap ---'
wc -l crates/perry-stdlib/src/readline/*.rs
rg -rn --iglob '*file*size*' -e '2000' scripts .github 2>/dev/null | head -20

Repository: PerryTS/perry

Length of output: 1634


🏁 Script executed:

#!/bin/bash
# Inspect module declarations and exact duplicates with a deterministic probe.
set -euo pipefail

echo '--- readline module declarations ---'
rg -n '^(pub )?mod ' crates/perry-stdlib/src/readline/mod.rs | head -20

echo '--- readline pump paths mentioned in source ---'
rg -n 'readline::|pump\.rs|mod pump|pump::' crates/perry-stdlib/src | head -80

echo '--- exact helper duplicate status ---'
python3 - <<'PY'
from pathlib import Path
mods = Path('crates/perry-stdlib/src/readline/mod.rs')
pump = Path('crates/perry-stdlib/src/readline/pump.rs')
for text in [mods.read_text(), pump.read_text()]:
    print('build_keypress_object count', text.count('fn build_keypress_object'))
    print('parse_keypress count', text.count('fn parse_keypress'))
    print('js_readline_process_pending count', text.count('pub extern "C" fn js_readline_process_pending'))
    print('js_readline_has_active count', text.count('pub extern "C" fn js_readline_has_active'))
# Extract source blocks, strip comments/whitespace for quick same-content check
import re
def extract_body(path, prefix):
    text = path.read_text()
    for pat, name in [(r'fn build_keypress_object\([^)]*\).*?\n\}', 'build')]:
        m = re.search(pat, text, re.S)
        if m:
            print(name, 'found ' + name)
PY

echo '--- scripts/check_file_size.sh nearby ---'
wc -l scripts/check_file_size.sh
sed -n '1,220p' scripts/check_file_size.sh

Repository: PerryTS/perry

Length of output: 12680


Prevent readline’s duplicate extern definitions and file-size violation.

crates/perry-stdlib/src/readline/mod.rs still defines js_readline_process_pending, js_readline_has_active, build_keypress_object, and parse_keypress alongside crates/perry-stdlib/src/readline/pump.rs. Since crates/perry-stdlib/src/lib.rs re-exports readline::*, these symbols are exposed from mod.rs while pump.rs is still a sibling file. Remove the pumped block from mod.rs and wire mod pump; with re-exports, so the externs are no longer duplicated and readline/mod.rs stays under the 2,000-line cap.

🤖 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-stdlib/src/readline/mod.rs` around lines 1533 - 1805, Remove
build_keypress_object, parse_keypress, js_readline_process_pending, and
js_readline_has_active from readline/mod.rs, then declare the pump module and
re-export its public symbols from mod.rs. Ensure the existing readline::* export
path continues exposing these APIs while keeping their single implementations in
pump.rs and bringing mod.rs below the 2,000-line limit.

proggeramlug pushed a commit that referenced this pull request Jul 27, 2026
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@proggeramlug
proggeramlug force-pushed the fix/linux-verification-sweep branch from 9ab8608 to 15fae2f Compare July 27, 2026 09:03

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with 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.

Inline comments:
In `@CONTRIBUTING.md`:
- Around line 25-30: Update the stale Node version reference in the parity-suite
guidance around the existing inline comment near line 88: replace the hardcoded
“Node 22” requirement with a reference to the exact version pinned in
.node-version, keeping the surrounding parity-test instructions unchanged.

In `@crates/perry/src/commands/check.rs`:
- Around line 197-211: Update the parse-error branch in the file-checking flow
to increment checked_files before continuing, alongside the Diagnostic::error
pushed to all_diagnostics. Ensure invalid files count as checked for
files_checked and progress accounting while preserving the existing blocking
diagnostic and continue behavior.

In `@docs/src/getting-started/installation.md`:
- Around line 13-36: Update the Debian/Ubuntu installation command in the Linux
toolchain section to install a supported clang version (15 or newer, such as the
documented versioned package) and the required libclang development package for
bindgen. Keep the existing linker/toolchain package and align the command with
the prerequisites documented in building.md.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1b32b95b-afd0-452d-b5a2-5d62096b4cce

📥 Commits

Reviewing files that changed from the base of the PR and between 9ab8608 and 15fae2f.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (17)
  • CLAUDE.md
  • CONTRIBUTING.md
  • Cargo.toml
  • changelog.d/6891-linux-verification-sweep.md
  • crates/perry-codegen/tests/native_proof_regressions.rs
  • crates/perry-hir/tests/c262_parity.rs
  • crates/perry-runtime/src/object/mod.rs
  • crates/perry-runtime/src/object/spill.rs
  • crates/perry-stdlib/src/crypto/random.rs
  • crates/perry-stdlib/src/readline/mod.rs
  • crates/perry-stdlib/src/readline/pump.rs
  • crates/perry/src/commands/check.rs
  • crates/perry/src/commands/compile/collect_modules.rs
  • docs/src/contributing/building.md
  • docs/src/getting-started/installation.md
  • run_parity_tests.sh
  • scripts/addr_class_ratchet_baseline.txt
🚧 Files skipped from review as they are similar to previous changes (3)
  • crates/perry-runtime/src/object/spill.rs
  • crates/perry-stdlib/src/readline/pump.rs
  • crates/perry-stdlib/src/readline/mod.rs

Comment thread CONTRIBUTING.md
Comment on lines +197 to +211
// A file that does not parse is a check FAILURE, not a file to
// skip. Before this, the error was printed only under `-v` and
// the file was dropped without touching `all_diagnostics`, so
// `error_count()` stayed 0 and `perry check` reported "All
// checks passed!" with exit 0 on syntactically invalid code —
// while `perry compile` correctly rejected the same file.
// Record it as a real diagnostic so the text summary, the JSON
// `success` field and the exit code all agree.
all_diagnostics.push(
Diagnostic::error(
DiagnosticCode::ParseError,
format!("{}: {}", canonical.display(), e),
)
.build(),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Count parse failures as checked files.

This branch records a blocking diagnostic but continues before checked_files += 1. A project containing only an invalid file therefore reports files_checked: 0, and progress accounting undercounts attempted files.

Proposed fix
             Err(e) => {
+                checked_files += 1;
                 // A file that does not parse is a check FAILURE, not a file to
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// A file that does not parse is a check FAILURE, not a file to
// skip. Before this, the error was printed only under `-v` and
// the file was dropped without touching `all_diagnostics`, so
// `error_count()` stayed 0 and `perry check` reported "All
// checks passed!" with exit 0 on syntactically invalid code —
// while `perry compile` correctly rejected the same file.
// Record it as a real diagnostic so the text summary, the JSON
// `success` field and the exit code all agree.
all_diagnostics.push(
Diagnostic::error(
DiagnosticCode::ParseError,
format!("{}: {}", canonical.display(), e),
)
.build(),
);
checked_files += 1;
// A file that does not parse is a check FAILURE, not a file to
// skip. Before this, the error was printed only under `-v` and
// the file was dropped without touching `all_diagnostics`, so
// `error_count()` stayed 0 and `perry check` reported "All
// checks passed!" with exit 0 on syntactically invalid code —
// while `perry compile` correctly rejected the same file.
// Record it as a real diagnostic so the text summary, the JSON
// `success` field and the exit code all agree.
all_diagnostics.push(
Diagnostic::error(
DiagnosticCode::ParseError,
format!("{}: {}", canonical.display(), e),
)
.build(),
);
🤖 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/src/commands/check.rs` around lines 197 - 211, Update the
parse-error branch in the file-checking flow to increment checked_files before
continuing, alongside the Diagnostic::error pushed to all_diagnostics. Ensure
invalid files count as checked for files_checked and progress accounting while
preserving the existing blocking diagnostic and continue behavior.

Comment thread docs/src/getting-started/installation.md
@proggeramlug

Copy link
Copy Markdown
Contributor Author

Pushed 1d075c9 addressing the CodeRabbit review — all 11 actionable findings and 6 of the 7 nitpicks. (The branch amend that landed mid-review already completed the mechanical pump.rs/spill.rs split wiring — the duplicate-externs/file-size finding — so this commit is the behavioral and GC fixes on top of it. readline/mod.rs is 1994 lines after keeping the tests plus adding new ones.)

Actionable findings fixed:

  • Arrow keys / unreachable 3-byte CSI branch — the pump now reassembles escape sequences that the raw-mode reader queues as 1-byte chunks (PENDING_ESCAPE accumulator carried across ticks, CSI/SS3 state machine). A bare ESC that nothing follows flushes on the next tick — a tick-granularity stand-in for Node's escapeCodeTimeout — and the held prefix counts as pending data in js_readline_has_active so the flush tick actually runs. New end-to-end tests: \x1b,[,A injected as three chunks → exactly one up keypress; bare-ESC flush. The stale pending_escape doc reference now describes something that exists.
  • readable busy loop — listeners fire only on ticks that delivered new chunks, plus a one-shot at EOF so a pull-mode consumer still gets its final wake-up. Test added.
  • Line dispatch ran JS inside the READLINE_INTERFACES borrow — callbacks are extracted under a short borrow and invoked after it drops. Same treatment for js_readline_prompt/js_readline_write, which had the identical pattern through call_write_value.
  • Unrooted pointers across allocating callsjs_readline_iterator, attach_custom_input, build_keypress_object and the keypress dispatch now root values in a RuntimeHandleScope and re-read through handles after every allocation (the spill.rs pattern). attach_custom_input also recomputes the raw stream pointer per .on call instead of reusing the top-of-function value.
  • Missing GC scanner registrationensure_gc_scanner_registered() now runs from js_readline_stdin_on, js_readline_create_interface and js_readline_iterator.
  • rl.close() clears LINE_CALLBACK/QUESTION_CALLBACK — no late 'line' after close.
  • rl.pause()/rl.resume() flip STDIN_PAUSED for the stdin-backed interface (custom streams still forward to node_stream), and the pump holds queued lines while paused so pausing actually stops 'line' delivery.
  • rl.write() writes the chunk through call_write_value(state.output, …).
  • destroy() clears READABLE_CALLBACKS (and the escape accumulator).

Nitpicks:

  • spill learned-width hook — moved off the steady-state path, but gated on the write raising the buffer's high-water mark (field_index >= length) rather than slow-path-only as proposed: the slow path allocates power-of-two headroom (first write to index 2 → capacity 8), so slow-path-only would never learn widths 4–8 from that instance. The length gate keeps learned widths byte-identical to the old unconditional call while making round-robin overflow writes TLS-free.
  • SAVED.lock().unwrap() → poison recovery at all 4 sites (unix + windows impls).
  • Interface slot leak — a closed custom interface now sets its slot to None, so the GC scanner stops rooting its input/output/callbacks. Deliberately not reusing indices: handles live in JS as plain numbers, and a stale handle to a closed interface must keep hitting the None slot (a no-op, analogous to Node's ERR_USE_AFTER_CLOSE) rather than silently aliasing whichever new interface recycled the index. Residual growth is one None per closed interface.
  • Tests spawning the real stdin readerensure_reader_started no longer spawns under cfg(test) (the flag still flips so has-active logic stays production-shaped), reset() now clears READER_STARTED, and the EOF-workaround comment is gone.
  • JSValue::bool(cond); per-chunk loop clones the listener lists once per drain and parses each chunk once, not once per callback.

Skipped (1):

  • Buffered reads + bounded queues (reader thread) — agreed it's real, but it's behavior-sensitive surgery on pre-existing code: the reader observes raw/cooked mode flips at byte granularity, and backpressure interacts with termios raw mode. It deserves its own issue/PR rather than a review-fix commit here.

Verification: check_file_size, addr-class ratchet and cargo fmt --check all green; perry-stdlib readline tests 16/16 (3 new). The parallel-run test flakes I hit locally on macOS (gc::teardown, webassembly namespace shapes, crypto::random macrotask drain, native_module_stream) reproduce identically at this branch's previous HEAD with the fixes reverted — pre-existing and unrelated.

Ralph Kuepper and others added 2 commits July 27, 2026 11:16
Found while verifying a fresh Linux checkout end-to-end. Everything here
except the `perry check` bug was already red on `main`.

- `perry check` reported "All checks passed!" and exit 0 on code that does
  not parse: the parse error was printed only under `-v`, then `continue`d
  without touching `all_diagnostics`, so `error_count()` stayed 0. Record a
  P001 diagnostic instead, so the text summary, the JSON `success` field and
  the exit code agree. `perry compile` already rejected these files.
- File-size gate (2000-line cap): split `object/mod.rs` 2039 -> 1709 by
  extracting the #6812 spill/overflow storage into `object/spill.rs`, and
  `readline.rs` 2066 -> 1796 by extracting the drain/pump + keypress decoding
  into `readline/pump.rs`. Code is unchanged; only visibility was widened so
  the existing `use super::*` sites keep resolving.
- rustfmt: blank line after the trailing comment in `collect_modules.rs` so
  rustfmt stops indenting the following comment block to column 40.
- Three stale test assertions, each verified behaviourally against Node
  26.5.0 first (no user-visible defect in any of them):
  * perry-hir expected the `??=` RHS store as `PutValueSet`; lowering emits
    `PropertySet` for a plain member target. Accept both — the invariant is
    *where* the store sits, not which node encodes it.
  * perry-codegen sliced `fast.inner.body`..`fast.inner.exit` for the numeric
    stores, but #6812's spill lanes moved them into `fast.store.spill.*` /
    `fast.store.inline.*` successors. Collect every
    `object_array_write.loop.fast.*` block instead.
  * perry-stdlib asserted `randomBytes`' callback fires synchronously; #6430
    deliberately deferred it to a macrotask to match Node's libuv timing.
    Drain the immediate queue, and assert it does NOT fire inline.
- run_parity_tests.sh: the default `test-files/` suite under
  PERRY_NO_AUTO_OPTIMIZE linked the prebuilt `full` stdlib with no
  `external-*` pumps, producing 7 false "NEW gap failure" reports
  (events/http/net/fetch) that all pass with auto-optimize. node-suite
  already compensated; do the same for the default suite.
- Docs: libclang and rustc >= 1.94 were undocumented build prerequisites,
  and installation.md required "clang >= 15 for codegen" while every
  per-distro command below it installed no clang. Also point Node at
  .node-version rather than the stale "22".
- addr-class ratchet baseline: record the already-fixed accessors.rs site
  and the readline path move.

Gap suite 386/395 with every remaining failure triaged in
known_failures.json. cargo test green for perry-parser/-hir/-transform/
-codegen/-dispatch/-api-manifest, perry-runtime --lib (1468), perry-stdlib
--lib, perry --bins (734). Both clippy scopes clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
changelog.d/6891-linux-verification-sweep.md (1)

3-3: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Split the long CI-gates sentence for readability.

This line combines several independent changes; use two sentences so the changelog is easier to scan.

🤖 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 `@changelog.d/6891-linux-verification-sweep.md` at line 3, Split the single
long changelog sentence beginning “Recovered three red CI gates on main” into
two sentences, grouping the related extraction changes separately from the
formatting and addr-class baseline updates. Preserve all existing details and
wording aside from the sentence boundary.

Source: Linters/SAST tools

🤖 Prompt for all review comments with 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.

Inline comments:
In `@crates/perry-stdlib/src/readline/mod.rs`:
- Around line 606-614: The close flow must not route a repeated close of a
custom-stream interface into the singleton stdin path after READLINE_INTERFACES
loses its slot. Preserve the closed custom-interface state in
READLINE_INTERFACES so js_readline_close continues selecting
close_custom_interface, whose existing state.closed guard makes subsequent
closes no-ops; add a regression test for double-closing a custom interface if
the surrounding test structure supports it.
- Around line 850-908: Remove the initial raw_ptr_from_value(input).is_none()
early return in attach_custom_input so child_process stdio pipes can reach the
!stream_is_readable(input_handle.get_nanbox_f64()) fallback and register
listeners through their own .on method. Preserve the native node_stream
registration path for readable streams, including its existing per-call raw
pointer validation.

---

Nitpick comments:
In `@changelog.d/6891-linux-verification-sweep.md`:
- Line 3: Split the single long changelog sentence beginning “Recovered three
red CI gates on main” into two sentences, grouping the related extraction
changes separately from the formatting and addr-class baseline updates. Preserve
all existing details and wording aside from the sentence boundary.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d5a17b0e-7b47-4d52-855e-a276db60bdc3

📥 Commits

Reviewing files that changed from the base of the PR and between 15fae2f and 1d075c9.

📒 Files selected for processing (4)
  • changelog.d/6891-linux-verification-sweep.md
  • crates/perry-runtime/src/object/spill.rs
  • crates/perry-stdlib/src/readline/mod.rs
  • crates/perry-stdlib/src/readline/pump.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • crates/perry-stdlib/src/readline/pump.rs
  • crates/perry-runtime/src/object/spill.rs

Comment on lines +606 to +614
// Release the slot so the GC scanner stops rooting the closed
// interface's input/output/callbacks. Handles are NOT reused: a stale
// handle to a closed interface must hit the `None` slot (a no-op, like
// Node's ERR_USE_AFTER_CLOSE), not alias a newer interface.
READLINE_INTERFACES.with(|interfaces| {
if let Some(slot) = interfaces.borrow_mut().get_mut(handle as usize) {
*slot = None;
}
});

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 | 🔴 Critical | ⚡ Quick win

Clearing the interface slot on close breaks routing for a later close() on the same handle, corrupting the unrelated stdin-backed interface's global state.

close_custom_interface now sets READLINE_INTERFACES[handle] = None (Line 611-613). But js_readline_close decides which close path to run via with_interface(_handle, |state| state.uses_custom_stream).unwrap_or(false) (Line 1322) — once the slot is None, this lookup fails and defaults to false (non-custom), so a second close() on the same handle (a plain double-close(), or an explicit rl.close() after the stream's own 'end'/'close' already invoked custom_input_closeclose_custom_interface) falls straight into the singleton-stdin close path below: it sets the global EOF_REACHED (killing js_readline_has_active/event-loop liveness for the entire stdin), wipes the shared QUESTION_CALLBACK/LINE_CALLBACK (used by the real stdin-backed interface, not this custom one), and can mis-fire the shared CLOSE_CALLBACK.

Before this change, the slot stayed populated with closed: true, so a repeat close still routed correctly to close_custom_interface, which already no-ops via if state.closed { None } else { ... }. Removing the slot instead of relying on that flag reintroduces the exact hazard the flag existed to prevent.

🐛 Proposed fix: don't let a missing custom-interface slot fall through to the stdin close path
 pub extern "C" fn js_readline_close(_handle: i64) -> f64 {
-    if with_interface(_handle, |state| state.uses_custom_stream).unwrap_or(false) {
-        close_custom_interface(_handle);
-        return undefined();
-    }
+    match with_interface(_handle, |state| state.uses_custom_stream) {
+        Some(true) => {
+            close_custom_interface(_handle);
+            return undefined();
+        }
+        None if _handle != STDIN_READLINE_HANDLE => {
+            // Slot already cleared by a prior close_custom_interface() call
+            // (double close(), or close() after the backing stream's own
+            // 'end'/'close' already tore it down) — a no-op, not a reason
+            // to run the stdin-backed close path against unrelated global
+            // state.
+            return undefined();
+        }
+        _ => {}
+    }
     EOF_REACHED.store(true, Ordering::Release);

Consider adding a regression test that calls close() twice on a custom-stream interface and asserts EOF_REACHED/QUESTION_CALLBACK are untouched.

Also applies to: 1321-1345

🤖 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-stdlib/src/readline/mod.rs` around lines 606 - 614, The close
flow must not route a repeated close of a custom-stream interface into the
singleton stdin path after READLINE_INTERFACES loses its slot. Preserve the
closed custom-interface state in READLINE_INTERFACES so js_readline_close
continues selecting close_custom_interface, whose existing state.closed guard
makes subsequent closes no-ops; add a regression test for double-closing a
custom interface if the surrounding test structure supports it.

Comment thread crates/perry-stdlib/src/readline/mod.rs
pump.rs:
- Reassemble ANSI escape sequences that the raw-mode reader queues as
  1-byte chunks (ESC, '[', 'A' -> one 'up' keypress) via a PENDING_ESCAPE
  accumulator carried across ticks; a bare ESC flushes on the next tick
  (tick-granularity stand-in for Node's escapeCodeTimeout), and the held
  prefix counts as pending data so the flush tick runs.
- 'readable' listeners fire only when a tick delivers new chunks, plus a
  one-shot at EOF, instead of on every event-loop iteration forever.
- Listener lists are cloned once per drain and each chunk parsed once
  (not once per callback); the keypress sequence string is rooted across
  the key-object build; build_keypress_object roots the object across its
  string allocations; JSValue::bool(cond) simplification.

readline/mod.rs:
- Line dispatch pulls callbacks under a short interface borrow and
  invokes them after it drops — a re-entrant close()/emit would panic the
  RefCell, and the GC scanner skips borrowed slots. Same treatment for
  js_readline_prompt / js_readline_write (via call_write_value).
- js_readline_iterator and attach_custom_input root values in a
  RuntimeHandleScope across allocating calls; attach_custom_input also
  recomputes the raw stream pointer per .on call.
- ensure_gc_scanner_registered() now also runs from js_readline_stdin_on,
  js_readline_create_interface and js_readline_iterator.
- rl.close() clears LINE/QUESTION callbacks (no late 'line' after close);
  rl.pause()/resume() gate STDIN_PAUSED for the stdin-backed interface
  and the pump holds queued lines while paused; rl.write() writes the
  chunk to the output stream; destroy() clears READABLE_CALLBACKS and the
  escape accumulator; closed custom interfaces release their slot
  (handles are not reused — a stale handle must no-op, not alias a newer
  interface); SAVED termios locks recover from poisoning.
- Tests never spawn the real stdin reader (cfg(test) gate), so reset()
  clears READER_STARTED; new tests: split-escape reassembly, bare-ESC
  flush, readable gating.

object/spill.rs:
- note_learned_inline_fields moves off the steady-state hot path: only
  writes that raise the spill buffer's high-water mark (or take the slow
  path) record it. Slow-path-only would under-learn inside the
  power-of-two capacity headroom; the length gate keeps learned widths
  identical while making round-robin overflow writes TLS-free.
@proggeramlug
proggeramlug force-pushed the fix/linux-verification-sweep branch from 1d075c9 to 81bb06d Compare July 27, 2026 09:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant