shell: remove unsafe from the interpreter, state nodes, IOWriter/IOReader and Builtin
What
Same programme as #40055 … #40221. Stacked on #40204 (base branch claude/subprocess-zero-unsafe; retarget to main once that lands). Every target in src/runtime/shell/ goes to 0: interpreter.rs 53 → 0, states/{Cmd,Expansion,CondExpr,Async,Base,Pipeline,Script,Stmt,Subshell}.rs → 0, IOWriter.rs 7 → 0, IOReader.rs 7 → 0, Builtin.rs 10 → 0, IO.rs 4 → 0, dispatch_tasks.rs 5 → 0; builtins touched on the way: mv.rs 8 → 0, touch.rs 6 → 0, export.rs 1 → 0, mkdir.rs 6 → 1, ls.rs 11 → 2, cp.rs 17 → 3 (rm.rs's DirTask tree is its own raw-pointer design and is left for a follow-up).
- State tree: nodes live in an append-only chunked slab on the
Interpreteraddressed by id; parent/child links are ids,child_donere-enters through the interpreter, and noRef/RefMuton a node, env, or captured buffer is held across a re-entrant call (builtins never hold their node borrowed acrossIOWriter::enqueue).ShellExecEnvhasDrop+clear();Bufio::Borrowed→Rc<RefCell<Vec>>. - Parsed script:
bun_shell_parser::ParsedScriptowns arena + root;root(&self) -> &Script<'_>is covariant/safe and the erased'staticform is only available asroot_backref() -> BackRef<Script<'static>>(one lifetime-only transmute with SAFETY in the parser crate). - IOWriter / IOReader: intrusively refcounted (
IOWriterRef/IOReaderRefRAII newtypes overRefPtr);impl_buffered_writer_parent!gets aborrow = thisentry whoseref_/derefhooks are the parent's intrusive count; pending-writer entries are typed; sync completions are deferred throughYield::OnIoWriterChunk. - Thread-pool / loop bounces:
bun_event_loop::EventLoopTask::arm_boxed<T, R>(Box<T>, node)(JS arm = manual-deinitConcurrentTaskwithT::TAG, mini arm = trampoline toR::run_from_loop_thread(Box<T>)),AnyTaskWithExtraContext::{arm_boxed, load}(also fixes a pre-existing free-under-&mutin the mini loop — it now copies the node out before running),boxed_taskable!,crate::shell_task!(Ty);ShellTask.interpis anInterpRefopenable only on the owning thread. Bounces use the embedded nodes (no allocation per bounce; miniAsyncis 0 allocs vs 1 on main). bun_ptr::SelfRoot/RefPtr::new_cycliccherry-picked from #40210;owned_task!/intrusive_work_task!/intrusive_field!accept a nested field path;EventLoopHandle::with_env;Body::Value::with_request_or_response(value, |v| ..);Stdio::Captureis a unit variant.
Behaviour notes: a parser-internal error with no diagnostics surfaces as ParseFailure::Other (thrown/returned) instead of an empty message; a pipeline/subshell child's duped env closes when its slot is freed (after Cmd::deinit rather than just before). Windows: a synchronous uv submission failure still re-enters IOWriter::on_error from inside write() exactly as on main (io-layer follow-up).
Perf (release, perf stat -e instructions:u, taskset-pinned, interleaved, 7 runs, medians; base = #40204): bun run --shell=bun script of 1000× true −0.15 %; 500× ([ -f ] + mkdir -p + touch + glob) +0.15 %; JS loop 3000× $`true` +0.35 %. (Node state uses a bun_ptr::JsRefCell — RefCell API/panics in debug, JsCell cost in release; node chunks are built in place with the first chunk inline.) No allocation per thread-pool or Async bounce (embedded nodes; mini Async is 0 allocs vs 1 on main).
Pre-existing bug fixed in passing: IOWriter::fail_pending_writers called each child back once while every output chunk of a builtin shared one ChildPtr, so mkdir -v a b c | head -1 / ls dir1 dir2 … | head -1 could hang after EPIPE; each parked OutputTask now has its own sequence number and the four per-builtin output vtables are one generic OutputTask::{start, write, on_chunk} (tests added).
Testing
Debug+ASAN: bunshell, commands/*, exec, throw, yield, lazy, leak, lex/parse/brace/glob/escape/env, file-io, epipe, pipeline_stack, worker-terminate-leak, seq-condexpr, sentinel, cmdsub-crash, blocking-pipe, read/write-fault, leak-args, shelloutput, bunshell-file/instance/default, run-shell, spawn-shell-signal, shell regression tests — 945 pass / 3 fail (root-user cases, identical on base) / 33 skip; new test rm error with stderr redirected to a file. Hand-driven: pipelines mixing builtins and subprocesses, ${Buffer}/${Response} redirects, &&/||/subshell/cmdsubst, brace+glob, .text/.lines/.quiet/.nothrow/.env/.cwd, throwing interpolation, Worker terminated mid-pipeline, 2000× echo hi then GC (ShellInterpreter count back to baseline) — exit codes correct, no ASAN output. clippy clean; source lints pass; rust-check-all windows-msvc + apple-darwin pass.