Skip to content

Commit 9837429

Browse files
committed
Refactor input dispatch and handling layers
1 parent d4942e1 commit 9837429

11 files changed

Lines changed: 1291 additions & 555 deletions

File tree

src/engraving/checks/ledger.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,3 +285,55 @@ fn revoking_an_unknown_serial_changes_nothing() {
285285
.is_some()
286286
);
287287
}
288+
289+
#[test]
290+
fn an_orphaned_scope_does_not_adopt_what_opens_after_it() {
291+
// A run stopped inside step 1 leaves its `Begin` unpaired. Step 2 opens
292+
// after it and belongs to the procedure, not to the step the walk happened
293+
// to die in — the outcome of the enclosing scope closes whatever it still
294+
// held open.
295+
let ledger = fold(vec![
296+
record(1, "/task:", State::Begin(Vec::new())),
297+
record(2, "/task:/1", State::Begin(Vec::new())),
298+
record(3, "/task:/1/helper:", State::Begin(Vec::new())),
299+
record(2, "/task:/1", State::Done(None)),
300+
record(4, "/task:/2", State::Begin(Vec::new())),
301+
]);
302+
303+
let entry = ledger
304+
.look(Serial(1), "/task:/2")
305+
.expect("step 2 keyed under its procedure");
306+
assert_eq!(entry.serial, Serial(4));
307+
}
308+
309+
#[test]
310+
fn a_resumed_scope_keeps_the_serial_it_was_entered_at() {
311+
// A scope opened after an orphan is found again on the next walk, so
312+
// resuming does not write a second `Begin` for it. It did, and each resume
313+
// then mis-parented the next, ratcheting a duplicate spine onto the trail
314+
// one pair of records at a time.
315+
let ledger = fold(vec![
316+
record(1, "/task:", State::Begin(Vec::new())),
317+
record(2, "/task:/1", State::Begin(Vec::new())),
318+
record(3, "/task:/1/helper:", State::Begin(Vec::new())),
319+
record(2, "/task:/1", State::Done(None)),
320+
record(4, "/task:/2", State::Begin(Vec::new())),
321+
record(0, "/", State::Stop),
322+
// The second walk re-enters the procedure and finishes the step it
323+
// finds standing, rather than opening a fresh one beside it.
324+
record(0, "/", State::Resume),
325+
record(1, "/task:", State::Begin(Vec::new())),
326+
record(4, "/task:/2", State::Done(None)),
327+
]);
328+
329+
assert_eq!(ledger.serial_for(Serial(1), "/task:/2"), Serial(4));
330+
let entry = ledger
331+
.look(Serial(1), "/task:/2")
332+
.expect("step 2 still keyed under its procedure after the resume");
333+
assert!(
334+
entry
335+
.outcome
336+
.is_some(),
337+
"the second walk's outcome lands on the entry the first walk opened"
338+
);
339+
}

src/engraving/checks/navigation.rs

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,133 @@ fn a_session_boundary_is_not_a_position() {
109109
Some(Position::Live)
110110
);
111111
}
112+
113+
// The same call, dispatched again by a resumed session that was stopped at the
114+
// argument prompt before it reached the callee.
115+
fn redispatched() -> Vec<Record> {
116+
vec![
117+
record(1, "/task:", State::Begin(Vec::new())),
118+
record(2, "/task:/1", State::Begin(Vec::new())),
119+
record(
120+
2,
121+
"/task:/1",
122+
State::Invoke(InvokeTarget::Procedure("check:".to_string())),
123+
),
124+
record(0, "/", State::Stop),
125+
record(0, "/", State::Resume),
126+
record(
127+
2,
128+
"/task:/1",
129+
State::Invoke(InvokeTarget::Procedure("check:".to_string())),
130+
),
131+
record(
132+
2,
133+
"/task:/1",
134+
State::Invoke(InvokeTarget::Procedure("verify:".to_string())),
135+
),
136+
]
137+
}
138+
139+
#[test]
140+
fn a_redispatched_call_is_one_place_to_stand() {
141+
// Two sessions reaching the same call wrote the dispatch line twice, and
142+
// pressing Up walked both showing the same thing each time. The trail keeps
143+
// them; the cursor stops on the last. A second call in the same step writes
144+
// a different line at the same address, and stands on its own.
145+
let records = redispatched();
146+
let trail = Trail::new(&records);
147+
148+
let at = trail
149+
.last()
150+
.expect("a position to open on");
151+
assert_eq!(at, Position::At(6));
152+
assert_eq!(trail.step(at, Motion::Up), Some(Position::At(5)));
153+
assert_eq!(
154+
trail.step(Position::At(5), Motion::Up),
155+
Some(Position::At(1))
156+
);
157+
}
158+
159+
#[test]
160+
fn two_invocations_of_one_procedure_both_stand() {
161+
// A procedure invoked twice records both at its own path, so the path is
162+
// not what tells two executions apart — the serial is. Collapsing by path
163+
// would have swallowed the first call's whole subtree.
164+
let records = vec![
165+
record(1, "/task:", State::Begin(Vec::new())),
166+
record(2, "/task:/1", State::Begin(Vec::new())),
167+
record(3, "/task:/check:", State::Begin(Vec::new())),
168+
record(3, "/task:/check:", State::Done(None)),
169+
record(2, "/task:/1", State::Done(None)),
170+
record(4, "/task:/2", State::Begin(Vec::new())),
171+
record(5, "/task:/check:", State::Begin(Vec::new())),
172+
record(5, "/task:/check:", State::Done(None)),
173+
record(4, "/task:/2", State::Done(None)),
174+
];
175+
let trail = Trail::new(&records);
176+
177+
let mut at = Position::At(8);
178+
for expected in [7, 6, 5, 4, 3, 2, 1, 0] {
179+
at = trail
180+
.step(at, Motion::Up)
181+
.expect("every record stands as its own position");
182+
assert_eq!(at, Position::At(expected));
183+
}
184+
}
185+
186+
#[test]
187+
fn an_amended_answer_is_the_only_one_review_reaches() {
188+
// Withdrawing an answer and giving a different one writes a second outcome
189+
// at the same path under a fresh serial. Navigating back shows the value
190+
// the position carries now; the one it replaced, the `Revoke` that took it
191+
// away, and the entry line above it are all recorded and none of them are
192+
// places to go.
193+
let records = vec![
194+
record(1, "/task:", State::Begin(Vec::new())),
195+
record(2, "/task:/1", State::Begin(Vec::new())),
196+
record(2, "/task:/1", State::Skip),
197+
record(2, "/task:/1", State::Revoke),
198+
record(3, "/task:/1", State::Begin(Vec::new())),
199+
record(3, "/task:/1", State::Done(None)),
200+
];
201+
let trail = Trail::new(&records);
202+
203+
let at = trail
204+
.last()
205+
.expect("a position to open on");
206+
assert_eq!(at, Position::At(5));
207+
assert_eq!(trail.step(at, Motion::Up), Some(Position::At(4)));
208+
assert_eq!(
209+
trail.step(Position::At(4), Motion::Up),
210+
Some(Position::At(0))
211+
);
212+
}
213+
214+
#[test]
215+
fn a_revoked_scope_takes_what_it_held_with_it() {
216+
// Revoking a call withdraws the whole subtree beneath it: the replay redoes
217+
// that work under fresh serials, so the records the first pass left are no
218+
// more current than the call that held them.
219+
let records = vec![
220+
record(1, "/task:", State::Begin(Vec::new())),
221+
record(2, "/task:/check:", State::Begin(Vec::new())),
222+
record(3, "/task:/check:/1", State::Begin(Vec::new())),
223+
record(3, "/task:/check:/1", State::Done(None)),
224+
record(2, "/task:/check:", State::Done(None)),
225+
record(2, "/task:/check:", State::Revoke),
226+
record(4, "/task:/check:", State::Begin(Vec::new())),
227+
record(5, "/task:/check:/1", State::Begin(Vec::new())),
228+
record(5, "/task:/check:/1", State::Done(None)),
229+
record(4, "/task:/check:", State::Done(None)),
230+
];
231+
let trail = Trail::new(&records);
232+
233+
let mut at = Position::At(9);
234+
for expected in [8, 7, 6, 0] {
235+
at = trail
236+
.step(at, Motion::Up)
237+
.expect("the standing execution walks back to the root");
238+
assert_eq!(at, Position::At(expected));
239+
}
240+
assert_eq!(trail.step(at, Motion::Up), None);
241+
}

src/engraving/ledger.rs

Lines changed: 16 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,8 @@ pub struct Entry {
1616
/// Results bound to variables coming out of scope.
1717
pub bound: Vec<Supplied>,
1818
pub outcome: Option<State>,
19-
/// This is set by a `Revoke` naming this entry, to be cleared by the next
20-
/// `Begin` at this address. Only the target of a revocation is marked,
21-
/// never an ancestor, as this is what stops `walk_invoke` otherwise
22-
/// thinking it has to restore the very argument being amended.
19+
/// Only the target of a revocation is marked, never an ancestor, which is
20+
/// what stops `walk_invoke` restoring the very argument being amended.
2321
pub revoked: bool,
2422
}
2523

@@ -103,13 +101,15 @@ impl Ledger {
103101
);
104102
}
105103
}
106-
if self
104+
// The stack pops back past this scope, dropping what it still
105+
// held open.
106+
if let Some(at) = self
107107
.open
108-
.last()
109-
== Some(&record.serial)
108+
.iter()
109+
.position(|s| *s == record.serial)
110110
{
111111
self.open
112-
.pop();
112+
.truncate(at);
113113
}
114114
}
115115
State::Revoke => self.revoke(record.serial),
@@ -123,9 +123,8 @@ impl Ledger {
123123
}
124124
}
125125

126-
// Ancestors have their outcome cleared too: a Section short-circuits
127-
// before descending, so its empty `Begin` would leave it standing.
128-
// Descendants are left alone, the input guard reaching them.
126+
// A Section short-circuits before descending, so an ancestor left standing
127+
// on its empty `Begin` would be skipped on the replay.
129128
fn revoke(&mut self, serial: Serial) {
130129
if let Some(key) = self.key_of(serial) {
131130
if let Some(entry) = self
@@ -136,9 +135,7 @@ impl Ledger {
136135
entry.revoked = true;
137136
}
138137
}
139-
// Retaining each entry, its serial and its `Begin` is what keeps the
140-
// descendants reachable and lets a revoked iteration still be claimed
141-
// by the item it recorded.
138+
// Descendants are left standing, their entries keeping them reachable.
142139
let mut at = serial;
143140
while let Some(parent) = self
144141
.scopes
@@ -160,13 +157,10 @@ impl Ledger {
160157
}
161158
}
162159

163-
// Re-entering a scope implicitly closes whatever was opened inside it, so
164-
// the stack pops back past it. Without this a resume after a mid-step Quit
165-
// parents the second walk's records under the step that was in flight.
166160
fn open_scope(&mut self, record: &Record, supplied: Vec<Supplied>) {
167-
// Re-entry keeps what the scope already recorded; only a scope being
168-
// opened afresh takes a new entry.
169161
let standing = self.standing(record.serial, &record.path, &supplied);
162+
// Without this, a resume after a mid-step Quit parents the second
163+
// walk's records under the step that was in flight.
170164
if let Some(at) = self
171165
.open
172166
.iter()
@@ -175,11 +169,8 @@ impl Ledger {
175169
self.open
176170
.truncate(at);
177171
}
178-
// A serial is allocated per (parent, edge) pair, so its parent is
179-
// fixed by its route and re-entering the scope does not move it. The
180-
// open stack answers only for a serial being seen for the first time —
181-
// which is what lets a scope replayed silently, writing no `Begin` of
182-
// its own, still have a stale descendant record beneath it correctly.
172+
// A serial is allocated per (parent, edge) pair, so re-entry does not
173+
// move it; the open stack answers only for one seen for the first time.
183174
let parent = match self
184175
.scopes
185176
.get(&record.serial)
@@ -347,12 +338,8 @@ impl Ledger {
347338
found
348339
}
349340

350-
/// The serial to enter this position at: the one a prior walk used if it
351-
/// reached here, otherwise a fresh number. Lookup-before-allocate is what
352-
/// keeps a resumed scope addressing its own recorded descendants.
353341
/// The serial to record work at this address under: the one it already
354-
/// wears while it still stands, a fresh one once revoked, redone work
355-
/// being new work.
342+
/// wears while it still stands, a fresh one once revoked.
356343
pub fn serial_for(&self, parent: Serial, path: &str) -> Serial {
357344
match self.look(parent, path) {
358345
Some(entry) if !entry.revoked => entry.serial,

0 commit comments

Comments
 (0)