Skip to content

Commit 6a990cc

Browse files
authored
Explicitly record full document completion in PFFTT (#124)
Record a new `Finish` state to complement the `Start` line, closing the run of a document. We had a Begin/Done pair around the entry procedure (if there was one) but nothing actually pairing with the start of the Technique. This came up when we wanted to embellish the interactive trace with a new visual convention: double arrows '⇒' and '⇐' are used to mark the entry and exit from a Technique document as a whole (in contrast to the single arrows which mark movement within the document) and we realized we didn't have a compliment for `Start`.
2 parents 23721f3 + 4d43abf commit 6a990cc

15 files changed

Lines changed: 199 additions & 24 deletions

File tree

src/parsing/checks/parser.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,17 @@ fn reading_invocations() {
535535
parameters: None
536536
})
537537
);
538+
539+
// Any scheme reads as external; a `:` cannot occur in a local identifier.
540+
input.initialize("<file://./OtherDoor.tq>");
541+
let result = input.read_invocation();
542+
assert_eq!(
543+
result,
544+
Ok(Invocation {
545+
target: Target::Remote(External::new("file://./OtherDoor.tq")),
546+
parameters: None
547+
})
548+
);
538549
}
539550

540551
#[test]

src/parsing/parser.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2152,14 +2152,16 @@ impl<'i> Parser<'i> {
21522152
Ok(symbol)
21532153
}
21542154

2155-
/// Parse a target like <procedure_name> or <https://example.com/proc>
2155+
/// Parse a target of either the form <procedure_name> or
2156+
/// <https://example.com/proc>. A `:` cannot appear in a local identifier,
2157+
/// so its presence marks the content as an external URI.
21562158
fn read_target(&mut self) -> Result<Target<'i>, ParsingError> {
21572159
let start_offset = self.offset;
21582160
self.take_block_chars("an invocation", '<', '>', true, |inner| {
21592161
let content = inner
21602162
.source
21612163
.trim();
2162-
if content.starts_with("https://") {
2164+
if content.contains(':') {
21632165
Ok(Target::Remote(External {
21642166
value: content,
21652167
span: inner.span_of(content),

src/runner/checks/runner.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,8 @@ fn step_outcomes_recorded() {
150150
.is_empty()
151151
})
152152
.collect();
153-
// Start + Begin + Done — three lines.
154-
assert_eq!(lines.len(), 3);
153+
// Start + Begin + Done + Finish — four lines.
154+
assert_eq!(lines.len(), 4);
155155
assert_eq!(
156156
lines[0],
157157
"2026-05-16T00:00:00Z 000001 / Start file:///tmp/Test.tq"
@@ -1411,8 +1411,9 @@ fn loop_inside_step_produces_one_result() {
14111411
.run(env)
14121412
.expect("run");
14131413

1414-
// One Start record, then the enclosing step's Begin and Done — the
1415-
// Loop inside the step body does not record events of its own.
1414+
// One Start record, then the enclosing step's Begin and Done, then the
1415+
// closing Finish — the Loop inside the step body does not record events of
1416+
// its own.
14161417
let pfftt = fixture.pfftt_contents();
14171418
let lines: Vec<&str> = pfftt
14181419
.lines()
@@ -1422,9 +1423,10 @@ fn loop_inside_step_produces_one_result() {
14221423
.is_empty()
14231424
})
14241425
.collect();
1425-
assert_eq!(lines.len(), 3);
1426+
assert_eq!(lines.len(), 4);
14261427
assert!(lines[1].ends_with(" Begin"));
14271428
assert!(lines[2].contains(" Done"));
1429+
assert!(lines[3].ends_with(" Finish"));
14281430
}
14291431

14301432
#[test]

src/runner/checks/state.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,17 @@ fn format_record_pins_on_disk_text() {
314314
"2026-05-17T00:28:25Z 015003 / Resume\n"
315315
);
316316

317+
let record = Record {
318+
recorded: "2026-05-17T00:28:30Z".to_string(),
319+
run_id: RunId(15003),
320+
path: "/".to_string(),
321+
state: State::Finish,
322+
};
323+
assert_eq!(
324+
format_record(&record),
325+
"2026-05-17T00:28:30Z 015003 / Finish\n"
326+
);
327+
317328
let record = Record {
318329
recorded: "2026-05-17T00:28:30Z".to_string(),
319330
run_id: RunId(15003),
@@ -487,6 +498,12 @@ fn record_round_trips_through_format_and_parse() {
487498
uri: "file:///foo/Bar.tq".to_string(),
488499
},
489500
},
501+
Record {
502+
recorded: "2026-05-17T00:28:25Z".to_string(),
503+
run_id: RunId(1),
504+
path: "/".to_string(),
505+
state: State::Finish,
506+
},
490507
Record {
491508
recorded: "2026-05-17T00:28:25Z".to_string(),
492509
run_id: RunId(1),

src/runner/driver.rs

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,14 @@ pub trait Driver {
5959
/// subroutine — with its Qualified Name (the `↘` marker).
6060
fn enter(&mut self, qualified: &str);
6161

62+
/// Cross into this document on the way in: the `⇒` boundary line carrying
63+
/// the document name, version, and run identifier (`/ NetworkProbe,1 000096`).
64+
fn commence(&mut self, label: &str);
65+
66+
/// Cross out of this document on the way out: the `⇐` boundary line carrying
67+
/// the run identifier (`/ 000096`).
68+
fn conclude(&mut self, label: &str);
69+
6270
/// Display a line of formatted content at the left margin.
6371
fn display(&mut self, content: &str);
6472

@@ -176,6 +184,17 @@ impl<W: Write> Driver for Console<W> {
176184
let _ = writeln!(self.output);
177185
}
178186

187+
fn commence(&mut self, label: &str) {
188+
let renderer = self.renderer();
189+
write_marker_line(&mut self.output, &format!("⇒ {}", label), renderer);
190+
let _ = writeln!(self.output);
191+
}
192+
193+
fn conclude(&mut self, label: &str) {
194+
let renderer = self.renderer();
195+
write_marker_line(&mut self.output, &format!("⇐ {}", label), renderer);
196+
}
197+
179198
fn display(&mut self, content: &str) {
180199
let _ = writeln!(self.output, "{}", content);
181200
let _ = writeln!(self.output);
@@ -205,7 +224,7 @@ impl<W: Write> Driver for Console<W> {
205224
}
206225

207226
fn external(&mut self, qualified: &str) -> UserInput {
208-
prompt(&mut self.output, qualified, "", &[], Value::Unitus)
227+
prompt(&mut self.output, qualified, "", &[], Value::Unitus)
209228
}
210229

211230
fn command(&mut self, qualified: &str, script: &str) -> UserInput {
@@ -1173,6 +1192,15 @@ impl<W: Write> Driver for Automatic<W> {
11731192
let _ = writeln!(self.output);
11741193
}
11751194

1195+
fn commence(&mut self, label: &str) {
1196+
write_marker_line(&mut self.output, &format!("⇒ {}", label), self.renderer);
1197+
let _ = writeln!(self.output);
1198+
}
1199+
1200+
fn conclude(&mut self, label: &str) {
1201+
write_marker_line(&mut self.output, &format!("⇐ {}", label), self.renderer);
1202+
}
1203+
11761204
fn display(&mut self, content: &str) {
11771205
let _ = writeln!(self.output, "{}", content);
11781206
let _ = writeln!(self.output);
@@ -1338,6 +1366,16 @@ impl<D: Driver, W: Write> Driver for Transcript<D, W> {
13381366
.enter(qualified);
13391367
}
13401368

1369+
fn commence(&mut self, label: &str) {
1370+
self.inner
1371+
.commence(label);
1372+
}
1373+
1374+
fn conclude(&mut self, label: &str) {
1375+
self.inner
1376+
.conclude(label);
1377+
}
1378+
13411379
fn display(&mut self, content: &str) {
13421380
self.inner
13431381
.display(content);
@@ -1458,6 +1496,10 @@ impl Driver for Headless {
14581496

14591497
fn enter(&mut self, _qualified: &str) {}
14601498

1499+
fn commence(&mut self, _label: &str) {}
1500+
1501+
fn conclude(&mut self, _label: &str) {}
1502+
14611503
fn display(&mut self, _content: &str) {}
14621504

14631505
fn announce(&mut self, _message: &str) {}
@@ -1607,6 +1649,10 @@ impl Driver for Mock {
16071649
});
16081650
}
16091651

1652+
fn commence(&mut self, _label: &str) {}
1653+
1654+
fn conclude(&mut self, _label: &str) {}
1655+
16101656
fn display(&mut self, content: &str) {
16111657
self.events
16121658
.push(Event::Display(content.to_string()));

src/runner/mod.rs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,14 @@ pub fn start<'i>(
4848
let pfftt = construct_state_path(&run_dir, document);
4949
let appender = Appender::open(pfftt, run_id)?;
5050
let completed = HashMap::new();
51+
let label = document_label(document);
5152
let outcome = match mode {
5253
Mode::Interactive => {
5354
if !std::io::stdout().is_terminal() {
5455
return Err(RunnerError::TerminalRequired);
5556
}
56-
let mut runner = Runner::new(program, appender, completed, Console::new(), library);
57+
let mut runner = Runner::new(program, appender, completed, Console::new(), library)
58+
.with_document(label);
5759
runner.run(env)?
5860
}
5961
Mode::Automatic => {
@@ -63,7 +65,8 @@ pub fn start<'i>(
6365
completed,
6466
Automatic::new(colour),
6567
library,
66-
);
68+
)
69+
.with_document(label);
6770
runner.run(env)?
6871
}
6972
};
@@ -133,8 +136,21 @@ pub fn resume<'i>(
133136
state: State::Resume,
134137
};
135138
appender.append(&record)?;
136-
let mut runner =
137-
Runner::new(program, appender, completed, Console::new(), library).with_inputs(inputs);
139+
let mut runner = Runner::new(program, appender, completed, Console::new(), library)
140+
.with_inputs(inputs)
141+
.with_document(document_label(&document));
138142
let env = Environment::new();
139143
runner.run(env)
140144
}
145+
146+
// The boundary trace lines name the document by its file stem (`NetworkProbe`
147+
// for `NetworkProbe.tq`), matching the PFFTT file the run writes to.
148+
fn document_label(document: &Path) -> String {
149+
document
150+
.file_stem()
151+
.map(|s| {
152+
s.to_string_lossy()
153+
.into_owned()
154+
})
155+
.unwrap_or_default()
156+
}

src/runner/runner.rs

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ pub struct Runner<'i, D: Driver> {
113113
path: QualifiedPath<'i>,
114114
library: Library,
115115
context: Context,
116+
document: Option<String>,
116117
}
117118

118119
impl<'i, D: Driver> Runner<'i, D> {
@@ -132,9 +133,17 @@ impl<'i, D: Driver> Runner<'i, D> {
132133
path: QualifiedPath::new(),
133134
library,
134135
context: Context::native(),
136+
document: None,
135137
}
136138
}
137139

140+
/// Name the source document so the run brackets its walk double arrow
141+
/// marked trace lines.
142+
pub fn with_document(mut self, document: String) -> Self {
143+
self.document = Some(document);
144+
self
145+
}
146+
138147
/// Seed the runner with the inputs recorded by a prior run — the values
139148
/// supplied to the entry procedure and to each invocation — so a resume
140149
/// restores them rather than re-prompting. Empty on a fresh run.
@@ -167,6 +176,17 @@ impl<'i, D: Driver> Runner<'i, D> {
167176
/// anonymous wrapper if the document is top-level Steps, otherwise
168177
/// the first declared procedure.
169178
pub fn run(&mut self, mut env: Environment) -> Result<Outcome, RunnerError> {
179+
if let Some(document) = &self.document {
180+
let label = format!(
181+
"/ {},1 #{}",
182+
document,
183+
self.appender
184+
.run_id()
185+
.render()
186+
);
187+
self.driver
188+
.commence(&label);
189+
}
170190
if let Some(metadata) = self
171191
.program
172192
.prelude
@@ -259,6 +279,27 @@ impl<'i, D: Driver> Runner<'i, D> {
259279
} else {
260280
result
261281
};
282+
// A run that walked to its end closes with a `Finish` record at the
283+
// root and the double arrow marker.
284+
if let Ok(outcome) = &result {
285+
if let Outcome::Stopped = outcome {
286+
} else {
287+
self.record_finish()?;
288+
if self
289+
.document
290+
.is_some()
291+
{
292+
let label = format!(
293+
"/ #{}",
294+
self.appender
295+
.run_id()
296+
.render()
297+
);
298+
self.driver
299+
.conclude(&label);
300+
}
301+
}
302+
}
262303
result
263304
}
264305

@@ -785,7 +826,7 @@ impl<'i, D: Driver> Runner<'i, D> {
785826
}
786827

787828
self.driver
788-
.settle("", &qualified, &input);
829+
.settle("", &qualified, &input);
789830
let outcome = outcome_from(input);
790831
self.appender
791832
.append(&Record {
@@ -839,11 +880,15 @@ impl<'i, D: Driver> Runner<'i, D> {
839880
Some(text) => Some(text.as_str()),
840881
None => None,
841882
};
883+
// Set the acquired `(name : forma)` off from the path with a
884+
// trailing space; an invocation prompt instead glues its arguments
885+
// straight to the `<callee>`.
886+
let prompt = format!("{qualified} ");
842887
let mut acquired = Vec::with_capacity(names.len());
843888
for name in names {
844889
match self
845890
.driver
846-
.acquire(&qualified, Some(name.value), forma)
891+
.acquire(&prompt, Some(name.value), forma)
847892
{
848893
UserInput::Done(value) => acquired.push(value),
849894
UserInput::Skip => {
@@ -1486,6 +1531,21 @@ impl<'i, D: Driver> Runner<'i, D> {
14861531
Ok(outcome)
14871532
}
14881533

1534+
/// Record a `Finish` at the root path, closing a run that walked to its end.
1535+
fn record_finish(&mut self) -> Result<(), RunnerError> {
1536+
let run_id = self
1537+
.appender
1538+
.run_id();
1539+
let record = Record {
1540+
recorded: now_iso8601(),
1541+
run_id,
1542+
path: "/".to_string(),
1543+
state: State::Finish,
1544+
};
1545+
self.appender
1546+
.append(&record)
1547+
}
1548+
14891549
/// Record a deliberate Stop at the root path and unwind the walk.
14901550
fn record_stop(&mut self) -> Result<Outcome, RunnerError> {
14911551
let run_id = self

0 commit comments

Comments
 (0)