From b6e44f21d6064df541c8ced206eee001c952756e Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Tue, 14 Jul 2026 10:48:13 +0100 Subject: [PATCH] =?UTF-8?q?fix(compile):=20PR=20#113=20round=207=20?= =?UTF-8?q?=E2=80=94=20validated=20overlay=20attachment,=20registry-only?= =?UTF-8?q?=20dispose?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Finding-by-finding (framing revision 13; bites via scripts/bite against fe04aa4): 1. attach_style_overlay validates the handle. A handle's translator follows edits to ITS buffer only, so attaching it to another buffer created a render view showing spans nobody maintains — rejected now, with the message naming the recorded owner and pointing at add_style_overlay for the target buffer. A disposed handle's translator is gone, so re-attachment resurrected rendering with frozen coordinates — the disposed state is shared across handle clones (FromLua clones) via Arc and attachment after dispose() fails, pointing at add_style_overlay for a fresh handle. Bite: r7f1 pins cross-buffer rejection, same-buffer acceptance, dispose-then-attach rejection, and both message shapes. 2. dispose() detaches the translator through the always-registered SharedRegistry; only the window cleanup rides the optional SharedCore. Pre-fix all cleanup lived inside the SharedCore branch, so an install-only/headless host got success with the translator left attached — paying on every edit for the buffer's lifetime. Registry-only unit asserts the buffer's view count returns to baseline (and stays there on double dispose); the acceptance-crate twin r7f2 builds the same install-only host and bites via the mod.rs swap (the in-crate unit vanishes with it). Gates: fmt; clippy workspace all-targets; lib 1535; crdt lib 1709; compile acceptance 65; crdt acceptance 3; m4 101; m6.4 15; m6.5 11; m6.8 8; GPU 59; workspace sweep 2526/0; git diff --check. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB --- docs/compile-mode-framing.md | 33 ++++++++++--- src/lua_bindings/mod.rs | 81 +++++++++++++++++++++++++++++++- tests/compile_mode_acceptance.rs | 81 ++++++++++++++++++++++++++++++++ 3 files changed, 187 insertions(+), 8 deletions(-) diff --git a/docs/compile-mode-framing.md b/docs/compile-mode-framing.md index 5127747..3cd2d2b 100644 --- a/docs/compile-mode-framing.md +++ b/docs/compile-mode-framing.md @@ -1,7 +1,24 @@ # Compile-mode — framing (Arc 5 stage 1, terminal) -**Revision 12 — 2026-07-14. Status: implemented on branch -`compile-mode` (PR #113); revisions 7–12 fold in PR rounds 1–6.** +**Revision 13 — 2026-07-14. Status: implemented on branch +`compile-mode` (PR #113); revisions 7–13 fold in PR rounds 1–7.** + +Revision 13 (PR #113 round 7, findings 1–2): overlay handle +attachment is validated — `attach_style_overlay` rejects a handle +whose recorded buffer differs from the target (its translator +follows edits to ITS buffer only; a cross-buffer render view showed +spans nobody maintains) and rejects a disposed handle (re-attachment +resurrected rendering without the translator); the disposed state is +shared across handle clones and both error messages point at +`add_style_overlay` as the fix. And `dispose()` no longer performs +the translator detach inside the optional `SharedCore` branch: the +window cleanup uses the core when present, while the detach goes +through the always-registered `SharedRegistry` — an +install-only/headless host previously got success with the +translator left attached (and paying per edit) for the buffer's +lifetime. Bites: cross-buffer + dispose-then-attach acceptance, and +a headless-host twin in the acceptance crate (the in-crate +registry-only unit vanishes under a mod.rs swap; the twin bites). Revision 12 (PR #113 round 6, findings 1–3): render-view attachment is idempotent and split-complete. Overlays expose an @@ -268,10 +285,14 @@ Everything below was verified by reading the code, not the roadmap. switches clear window overlays; `attach_style_overlay(buf, handle)` re-attaches the render view, idempotently per window via the store identity, and same-buffer splits copy the render view to - the new pane (Revision 12). The handle has `add`, `clear`, - `clear_before`, `spans`, and idempotent `dispose` (teardown of the - translator + every window render view; one handle per buffer - incarnation needs no disposal). + the new pane (Revision 12). Attachment validates the handle: + wrong-buffer and disposed handles are rejected with messages + pointing at `add_style_overlay` (Revision 13). The handle has + `add`, `clear`, `clear_before`, `spans`, and idempotent `dispose` + (teardown of the translator + every window render view; the + translator detach rides the always-registered registry, not the + optional editor core; one handle per buffer incarnation needs no + disposal). - **Buffer-switch hooks**: `buffer.after-switch` exists and fires on the ordinary switch paths (recentf subscribes, `builtin/runtime/recentf.lua:54`). **`pmacs.editor.jump_back` does diff --git a/src/lua_bindings/mod.rs b/src/lua_bindings/mod.rs index f0f08cb..279c374 100644 --- a/src/lua_bindings/mod.rs +++ b/src/lua_bindings/mod.rs @@ -1754,11 +1754,19 @@ pub struct InterceptHandleLua { pub struct StyleOverlayHandleLua { /// Shared style spans rendered by every attached overlay view. spans: crate::overlay::SharedBufferStyleSpans, - /// Buffer the translator was attached to. + /// Buffer the translator was attached to. Attachment is + /// validated against this (round-7 finding 1): a render view on + /// any OTHER buffer would show coordinates translated only by + /// edits to this one. buffer: BufferId, /// The buffer-attached translator's view id — retained so /// `dispose()` can detach it. translator: crate::buffer::ViewId, + /// Shared across handle clones (`FromLua` clones): set by + /// `dispose()`, checked by attachment — re-attaching a disposed + /// handle would resurrect rendering without its translator + /// (round-7 finding 1). + disposed: Arc, } impl FromLua for StyleOverlayHandleLua { @@ -1843,13 +1851,23 @@ impl UserData for StyleOverlayHandleLua { // repeated creation on a long-lived buffer. Safe to call // twice; safe after the buffer is gone. methods.add_method("dispose", |lua, this, ()| { + this.disposed + .store(true, std::sync::atomic::Ordering::Relaxed); let id = crate::overlay::style_store_identity(&this.spans); + // Window cleanup needs the editor core, which is + // optional app data... if let Some(core) = lua.app_data_ref::() { let mut core = core.borrow_mut(); for win in core.windows.values_mut() { win.overlays.retain(|v| v.overlay_identity() != Some(id)); } - let registry = core.registry.clone(); + } + // ...but the translator detach must not go through it: + // an install-only/headless host registers the registry + // WITHOUT a core, and returning success while the + // translator stays attached would leak per-edit work for + // the buffer's lifetime (round-7 finding 2). + if let Some(registry) = lua.app_data_ref::() { let mut r = registry.borrow_mut(); if let Ok(buf) = r.get_mut(this.buffer) { buf.detach_view(this.translator); @@ -2977,6 +2995,7 @@ fn install_buffer_module(lua: &Lua, registry: &SharedRegistry) -> mlua::Result mlua::Result