From 7562d8319831b4dfcf8a3daf2a8721b038bb0864 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Tue, 14 Jul 2026 10:57:14 +0100 Subject: [PATCH] fix(compile): make overlay teardown atomic --- docs/compile-mode-framing.md | 23 ++++-- src/lua_bindings/mod.rs | 50 +++++++++++-- tests/compile_mode_acceptance.rs | 124 +++++++++++++++++++++++++++++++ 3 files changed, 185 insertions(+), 12 deletions(-) diff --git a/docs/compile-mode-framing.md b/docs/compile-mode-framing.md index 3cd2d2b..5d2addc 100644 --- a/docs/compile-mode-framing.md +++ b/docs/compile-mode-framing.md @@ -1,7 +1,18 @@ # Compile-mode — framing (Arc 5 stage 1, terminal) -**Revision 13 — 2026-07-14. Status: implemented on branch -`compile-mode` (PR #113); revisions 7–13 fold in PR rounds 1–7.** +**Revision 14 — 2026-07-14. Status: implemented on branch +`compile-mode` (PR #113); revisions 7–14 fold in PR rounds 1–8.** + +Revision 14 (PR #113 round 8, direct review fixes): overlay disposal +now preflights both the optional editor-core borrow and the required +registry borrow before changing the shared disposed flag or removing +either view. A re-entrant callback therefore receives a pointed, +retryable error instead of a `RefCell` panic or partial teardown; the +acceptance bite holds each borrow in turn, proves the handle remains +fully live, and retries successfully. `attach_style_overlay` also +resolves the recorded owner in the registry after its identity checks, +so a handle whose buffer (and translator) has died is rejected as +stale rather than reporting a successful no-op. Revision 13 (PR #113 round 7, findings 1–2): overlay handle attachment is validated — `attach_style_overlay` rejects a handle @@ -287,12 +298,14 @@ Everything below was verified by reading the code, not the roadmap. the store identity, and same-buffer splits copy the render view to 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 + pointing at `add_style_overlay`; a handle whose recorded owner has + died is rejected as stale (Revisions 13–14). 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). + optional editor core; teardown preflights both borrows so re-entrant + calls fail atomically and can be retried; 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 279c374..0ad2752 100644 --- a/src/lua_bindings/mod.rs +++ b/src/lua_bindings/mod.rs @@ -1101,6 +1101,16 @@ pub enum BindingError { after the edit completes" )] Reentrant, + + /// Style-overlay teardown was requested from a callback that is + /// still running under an editor-core or buffer-registry borrow. + /// Disposal touches both stores, so it must acquire both before + /// changing the shared disposed flag or removing either view. + #[error( + "style overlay disposal cannot run while editor state is borrowed; defer dispose() until \ + after the current callback completes" + )] + StyleOverlayDisposeReentrant, } // --------------------------------------------------------------------------- @@ -1851,13 +1861,32 @@ 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, ()| { + // Preflight every borrow before changing shared state. + // A callback may run while the editor core or registry is + // already borrowed; panicking (or removing the window + // views before discovering a registry conflict) would + // leave a partially disposed handle. Returning a pointed + // error keeps the operation retryable after the callback. + let core_handle = lua.app_data_ref::(); + let mut core = match core_handle.as_deref() { + Some(core) => Some(core.try_borrow_mut().map_err(|_| { + mlua::Error::external(BindingError::StyleOverlayDisposeReentrant) + })?), + None => None, + }; + let registry_handle = lua + .app_data_ref::() + .ok_or_else(|| mlua::Error::external(BindingError::NoRegistry))?; + let mut registry = registry_handle + .try_borrow_mut() + .map_err(|_| mlua::Error::external(BindingError::StyleOverlayDisposeReentrant))?; + 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(); + if let Some(core) = core.as_mut() { for win in core.windows.values_mut() { win.overlays.retain(|v| v.overlay_identity() != Some(id)); } @@ -1867,11 +1896,8 @@ impl UserData for StyleOverlayHandleLua { // 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); - } + if let Ok(buf) = registry.get_mut(this.buffer) { + buf.detach_view(this.translator); } Ok(()) }); @@ -3029,6 +3055,16 @@ fn install_buffer_module(lua: &Lua, registry: &SharedRegistry) -> mlua::Result