fix(compile): PR #113 round 7 — validated overlay attachment, registry-only dispose
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<AtomicBool> 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
This commit is contained in:
parent
fe04aa481b
commit
b6e44f21d6
|
|
@ -1,7 +1,24 @@
|
||||||
# Compile-mode — framing (Arc 5 stage 1, terminal)
|
# Compile-mode — framing (Arc 5 stage 1, terminal)
|
||||||
|
|
||||||
**Revision 12 — 2026-07-14. Status: implemented on branch
|
**Revision 13 — 2026-07-14. Status: implemented on branch
|
||||||
`compile-mode` (PR #113); revisions 7–12 fold in PR rounds 1–6.**
|
`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
|
Revision 12 (PR #113 round 6, findings 1–3): render-view attachment
|
||||||
is idempotent and split-complete. Overlays expose an
|
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,
|
switches clear window overlays; `attach_style_overlay(buf,
|
||||||
handle)` re-attaches the render view, idempotently per window via
|
handle)` re-attaches the render view, idempotently per window via
|
||||||
the store identity, and same-buffer splits copy the render view to
|
the store identity, and same-buffer splits copy the render view to
|
||||||
the new pane (Revision 12). The handle has `add`, `clear`,
|
the new pane (Revision 12). Attachment validates the handle:
|
||||||
`clear_before`, `spans`, and idempotent `dispose` (teardown of the
|
wrong-buffer and disposed handles are rejected with messages
|
||||||
translator + every window render view; one handle per buffer
|
pointing at `add_style_overlay` (Revision 13). The handle has
|
||||||
incarnation needs no disposal).
|
`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
|
- **Buffer-switch hooks**: `buffer.after-switch` exists and fires on
|
||||||
the ordinary switch paths (recentf subscribes,
|
the ordinary switch paths (recentf subscribes,
|
||||||
`builtin/runtime/recentf.lua:54`). **`pmacs.editor.jump_back` does
|
`builtin/runtime/recentf.lua:54`). **`pmacs.editor.jump_back` does
|
||||||
|
|
|
||||||
|
|
@ -1754,11 +1754,19 @@ pub struct InterceptHandleLua {
|
||||||
pub struct StyleOverlayHandleLua {
|
pub struct StyleOverlayHandleLua {
|
||||||
/// Shared style spans rendered by every attached overlay view.
|
/// Shared style spans rendered by every attached overlay view.
|
||||||
spans: crate::overlay::SharedBufferStyleSpans,
|
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,
|
buffer: BufferId,
|
||||||
/// The buffer-attached translator's view id — retained so
|
/// The buffer-attached translator's view id — retained so
|
||||||
/// `dispose()` can detach it.
|
/// `dispose()` can detach it.
|
||||||
translator: crate::buffer::ViewId,
|
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<std::sync::atomic::AtomicBool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromLua for StyleOverlayHandleLua {
|
impl FromLua for StyleOverlayHandleLua {
|
||||||
|
|
@ -1843,13 +1851,23 @@ impl UserData for StyleOverlayHandleLua {
|
||||||
// repeated creation on a long-lived buffer. Safe to call
|
// repeated creation on a long-lived buffer. Safe to call
|
||||||
// twice; safe after the buffer is gone.
|
// twice; safe after the buffer is gone.
|
||||||
methods.add_method("dispose", |lua, this, ()| {
|
methods.add_method("dispose", |lua, this, ()| {
|
||||||
|
this.disposed
|
||||||
|
.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||||
let id = crate::overlay::style_store_identity(&this.spans);
|
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::<SharedCore>() {
|
if let Some(core) = lua.app_data_ref::<SharedCore>() {
|
||||||
let mut core = core.borrow_mut();
|
let mut core = core.borrow_mut();
|
||||||
for win in core.windows.values_mut() {
|
for win in core.windows.values_mut() {
|
||||||
win.overlays.retain(|v| v.overlay_identity() != Some(id));
|
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::<SharedRegistry>() {
|
||||||
let mut r = registry.borrow_mut();
|
let mut r = registry.borrow_mut();
|
||||||
if let Ok(buf) = r.get_mut(this.buffer) {
|
if let Ok(buf) = r.get_mut(this.buffer) {
|
||||||
buf.detach_view(this.translator);
|
buf.detach_view(this.translator);
|
||||||
|
|
@ -2977,6 +2995,7 @@ fn install_buffer_module(lua: &Lua, registry: &SharedRegistry) -> mlua::Result<T
|
||||||
spans: Arc::clone(&spans),
|
spans: Arc::clone(&spans),
|
||||||
buffer: id.0,
|
buffer: id.0,
|
||||||
translator,
|
translator,
|
||||||
|
disposed: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||||
};
|
};
|
||||||
attach_style_overlay_to_visible_windows(lua, id.0, &spans);
|
attach_style_overlay_to_visible_windows(lua, id.0, &spans);
|
||||||
Ok(handle)
|
Ok(handle)
|
||||||
|
|
@ -2990,6 +3009,26 @@ fn install_buffer_module(lua: &Lua, registry: &SharedRegistry) -> mlua::Result<T
|
||||||
"attach_style_overlay",
|
"attach_style_overlay",
|
||||||
lua.create_function(
|
lua.create_function(
|
||||||
move |lua, (id, handle): (BufferIdLua, StyleOverlayHandleLua)| {
|
move |lua, (id, handle): (BufferIdLua, StyleOverlayHandleLua)| {
|
||||||
|
// Round-7 finding 1: a disposed handle's
|
||||||
|
// translator is gone — re-attaching would
|
||||||
|
// resurrect rendering with frozen coordinates —
|
||||||
|
// and a handle's translator follows edits to ITS
|
||||||
|
// buffer only, so attaching to any other buffer
|
||||||
|
// shows unmaintained spans.
|
||||||
|
if handle.disposed.load(std::sync::atomic::Ordering::Relaxed) {
|
||||||
|
return Err(mlua::Error::external(
|
||||||
|
"this style overlay handle was disposed; create a fresh \
|
||||||
|
one with pmacs.buffer.add_style_overlay",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if id.0 != handle.buffer {
|
||||||
|
return Err(mlua::Error::external(format!(
|
||||||
|
"this style overlay handle belongs to buffer {:?}; its \
|
||||||
|
spans are not translated by edits to {:?} — create an \
|
||||||
|
overlay for that buffer with pmacs.buffer.add_style_overlay",
|
||||||
|
handle.buffer, id.0
|
||||||
|
)));
|
||||||
|
}
|
||||||
attach_style_overlay_to_visible_windows(lua, id.0, &handle.spans);
|
attach_style_overlay_to_visible_windows(lua, id.0, &handle.spans);
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
|
|
@ -14848,4 +14887,42 @@ mod tests {
|
||||||
id.uptime_secs
|
id.uptime_secs
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn style_overlay_dispose_detaches_translator_without_a_core() {
|
||||||
|
// PR #113 round-7 finding 2: `fresh()` is the install-only /
|
||||||
|
// headless host shape — the registry is registered as app
|
||||||
|
// data, SharedCore is NOT. dispose() must detach the
|
||||||
|
// buffer-attached translator through the registry alone;
|
||||||
|
// pre-fix it returned success having done nothing, leaving
|
||||||
|
// the translator attached (and paying per edit) for the
|
||||||
|
// buffer's lifetime.
|
||||||
|
let (lua, reg, _cmds, _kms, _hks) = fresh();
|
||||||
|
lua.load(r#"_G.hbuf = pmacs.buffer.create("headless")"#)
|
||||||
|
.exec()
|
||||||
|
.unwrap();
|
||||||
|
let id = reg
|
||||||
|
.borrow()
|
||||||
|
.find_by_name("headless")
|
||||||
|
.expect("buffer exists");
|
||||||
|
let baseline = reg.borrow().get(id).unwrap().view_count();
|
||||||
|
lua.load(r"_G.hov = pmacs.buffer.add_style_overlay(_G.hbuf)")
|
||||||
|
.exec()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
reg.borrow().get(id).unwrap().view_count(),
|
||||||
|
baseline + 1,
|
||||||
|
"add_style_overlay attaches the translator"
|
||||||
|
);
|
||||||
|
lua.load("_G.hov:dispose()").exec().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
reg.borrow().get(id).unwrap().view_count(),
|
||||||
|
baseline,
|
||||||
|
"dispose must detach the translator with no core registered"
|
||||||
|
);
|
||||||
|
// Idempotent: a second dispose neither errors nor
|
||||||
|
// over-detaches.
|
||||||
|
lua.load("_G.hov:dispose()").exec().unwrap();
|
||||||
|
assert_eq!(reg.borrow().get(id).unwrap().view_count(), baseline);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2513,6 +2513,87 @@ fn r6f3_handle_dispose_detaches_translator_and_render_views() {
|
||||||
assert_eq!(render_views, 0, "dispose removes the window render views");
|
assert_eq!(render_views, 0, "dispose removes the window render views");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// PR #113 round 7 — bite tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn r7f1_attach_validates_buffer_identity_and_disposed_state() {
|
||||||
|
// Round-7 finding 1: a handle's translator follows edits to ITS
|
||||||
|
// buffer only, so attaching the handle to another buffer showed
|
||||||
|
// spans nobody maintains; attaching after dispose() resurrected
|
||||||
|
// rendering without the translator. Both now fail clearly, with
|
||||||
|
// the message pointing at add_style_overlay.
|
||||||
|
let s = editor();
|
||||||
|
let (ok_cross, err_cross, ok_same, ok_after, err_after): (bool, String, bool, bool, String) =
|
||||||
|
eval(
|
||||||
|
&s,
|
||||||
|
r#"
|
||||||
|
local a = pmacs.buffer.create("*ov-a*")
|
||||||
|
local b = pmacs.buffer.create("*ov-b*")
|
||||||
|
local ov = pmacs.buffer.add_style_overlay(a)
|
||||||
|
local ok_cross, err_cross = pcall(pmacs.buffer.attach_style_overlay, b, ov)
|
||||||
|
local ok_same = pcall(pmacs.buffer.attach_style_overlay, a, ov)
|
||||||
|
ov:dispose()
|
||||||
|
local ok_after, err_after = pcall(pmacs.buffer.attach_style_overlay, a, ov)
|
||||||
|
return ok_cross, tostring(err_cross), ok_same, ok_after, tostring(err_after)
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
assert!(!ok_cross, "cross-buffer attachment must be rejected");
|
||||||
|
assert!(
|
||||||
|
err_cross.contains("belongs to buffer") && err_cross.contains("add_style_overlay"),
|
||||||
|
"pointed message naming the fix; got: {err_cross}"
|
||||||
|
);
|
||||||
|
assert!(ok_same, "same-buffer attachment stays valid");
|
||||||
|
assert!(!ok_after, "attachment after dispose must be rejected");
|
||||||
|
assert!(
|
||||||
|
err_after.contains("disposed") && err_after.contains("add_style_overlay"),
|
||||||
|
"pointed message naming the fix; got: {err_after}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn r7f2_dispose_detaches_translator_in_a_headless_host() {
|
||||||
|
// Round-7 finding 2, acceptance twin (the in-crate unit vanishes
|
||||||
|
// with a mod.rs swap; this one bites): an install-only host
|
||||||
|
// registers the buffer registry as app data but NO editor core.
|
||||||
|
// Pre-fix, dispose() did all cleanup inside the optional
|
||||||
|
// SharedCore branch — returning success while the translator
|
||||||
|
// stayed attached for the buffer's lifetime.
|
||||||
|
use pmacs::lua_bindings::{
|
||||||
|
SharedCommandRegistry, SharedHookRegistry, SharedKeymapStack, SharedMenuRegistry,
|
||||||
|
SharedRegistry, install,
|
||||||
|
};
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::rc::Rc;
|
||||||
|
let lua = mlua::Lua::new();
|
||||||
|
let reg: SharedRegistry = Rc::new(RefCell::new(pmacs::buffer_registry::BufferRegistry::new()));
|
||||||
|
let cmds: SharedCommandRegistry = Rc::new(RefCell::new(pmacs::command::CommandRegistry::new()));
|
||||||
|
let kms: SharedKeymapStack = Rc::new(RefCell::new(pmacs::keymap_stack::KeymapStack::new()));
|
||||||
|
let mns: SharedMenuRegistry = Rc::new(RefCell::new(pmacs::menu::MenuRegistry::new()));
|
||||||
|
let hks: SharedHookRegistry = Rc::new(RefCell::new(pmacs::hook::HookRegistry::new()));
|
||||||
|
install(&lua, ®, &cmds, &kms, &mns, &hks).expect("install-only host");
|
||||||
|
lua.load(
|
||||||
|
r#"
|
||||||
|
_G.hbuf = pmacs.buffer.create("headless")
|
||||||
|
_G.hov = pmacs.buffer.add_style_overlay(_G.hbuf)
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.exec()
|
||||||
|
.expect("create + overlay");
|
||||||
|
let id = reg
|
||||||
|
.borrow()
|
||||||
|
.find_by_name("headless")
|
||||||
|
.expect("buffer exists");
|
||||||
|
let with_translator = reg.borrow().get(id).unwrap().view_count();
|
||||||
|
lua.load("_G.hov:dispose()").exec().expect("dispose");
|
||||||
|
assert_eq!(
|
||||||
|
reg.borrow().get(id).unwrap().view_count(),
|
||||||
|
with_translator - 1,
|
||||||
|
"dispose must detach the translator without a core registered"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn r5f3_tracked_line_start_matches_the_scan_across_transitions() {
|
fn r5f3_tracked_line_start_matches_the_scan_across_transitions() {
|
||||||
// Round-5 finding 3 is a performance fix — the per-CR/BS/erase
|
// Round-5 finding 3 is a performance fix — the per-CR/BS/erase
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue