# Field notes — smarm context-switch session (v0.8) Notes from one real session using llmdbg v0.8 to profile and verify smarm's naked-asm context switch (the TLS-accessor perf question and the XMM-not-saved correctness question). Recorded as friction-and-wishlist, not a defect report — nothing here blocked the work; every rough edge had a workaround. ## What worked - **Demangling.** `addr("switch_to_scheduler")` resolving directly meant zero `nm | grep` for mangled names whose hash changes every rebuild. Biggest single quality-of-life win. - **`trace({over, range, capture})`.** The capture closure returning a structured record per step (`{pc, insn, len, is_call}`) and letting the caller format it is the right shape — produced a clean 16-line shim trace in one pass. - **`next()` vs `step()`.** The distinction the docs emphasise is exactly the axis this task needed: stepping *into* the accessors (164 insns of debug-build `LocalKey` ceremony) vs *over* them is the difference between readable and unreadable, and `step()` *through* the stack-swapping `ret` is what let me walk the switch itself. - **The worked example** (`examples/smarm_ctx_switch_next.js`) was close enough to the real task to adapt directly. Good investment. ## What bit me - **Range checks on `state.pc` are treacherous when helpers sit adjacent to the function under test.** `set_actor_sp` lived at `sched+0x50`, *inside* my `+0x60` window, so a "stop when pc returns to the shim" loop terminated the instant it entered the accessor. Cost me two attempts before I abandoned range-based exit detection and dumped a flat `step()` stream to count by eye. The missing primitive is symbol *extent* (start ≤ pc < start+size), not symbol *address*. - **The `push rbx` consumed-by-breakpoint quirk** is correctly documented, but I had to re-apply "the first observed instruction is the second one" mentally at every session. A documented footgun is still a footgun. - **No float/vector register access.** The XMM-not-saved verification had to leave llmdbg entirely and fall back to `objdump` to confirm the compiler spills live XMM around the switch call. For a tool whose reason for existing is x86-64 context-switch correctness, watching `xmm0..15` across a switch feels squarely in scope — it would have turned a static inference into a dynamic proof (XMM dead at switch entry, live again after reload). ## Wishlist (rough priority order) 1. **Symbol-extent queries** — `symStart(pc)` / `symSize(pc)`, or an `inFn(pc, "name")` membership test. Fixes the range-check footgun directly. This is the highest-value add for asm work, where "am I still inside this function" is a constant question. Pure-JS-expressible if the native `sym` layer exposes extents. 2. **`countTo(stop, {over})`** — step until an address/predicate and return just the instruction count, instead of hand-rolling `let n=0; while(...){ step(); n++ }` and getting the boundary wrong. Counting per-call instruction cost was the core measurement of this task and had no first-class support. Pure-JS prelude helper. 3. **XMM/SSE register reads** — `state.xmm[i]` or `reg("xmm0")`. Needs Rust-side support (ptrace `GETFPREGS`/`xsave`). In scope for this tool's purpose. 4. **Prologue-aware breakpoints** — a mode (or `bp.setAfterPrologue()`) that removes the "first instruction eaten" caveat rather than documenting around it. 5. **`hexdump(addr, n)`** — peek at raw memory, e.g. a freshly-built initial actor stack from `init_actor_stack`. Pure-JS over a `readMem` primitive. Nice-to-have; didn't strictly need it this round. ## Suggested next step Items 1, 2, and 5 are pure-JS and fit the SPEC ethos ("everything expressible in JS lives in JS") — they belong in `src/prelude.js` and would be a clean v0.9 with no Rust changes (assuming `sym` extents and a `readMem` primitive are reachable from JS). Items 3 and 4 need native support and are larger.