Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/parsing/checks/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,17 @@ fn reading_invocations() {
parameters: None
})
);

// Any scheme reads as external; a `:` cannot occur in a local identifier.
input.initialize("<file://./OtherDoor.tq>");
let result = input.read_invocation();
assert_eq!(
result,
Ok(Invocation {
target: Target::Remote(External::new("file://./OtherDoor.tq")),
parameters: None
})
);
}

#[test]
Expand Down
6 changes: 4 additions & 2 deletions src/parsing/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2152,14 +2152,16 @@ impl<'i> Parser<'i> {
Ok(symbol)
}

/// Parse a target like <procedure_name> or <https://example.com/proc>
/// Parse a target of either the form <procedure_name> or
/// <https://example.com/proc>. A `:` cannot appear in a local identifier,
/// so its presence marks the content as an external URI.
fn read_target(&mut self) -> Result<Target<'i>, ParsingError> {
let start_offset = self.offset;
self.take_block_chars("an invocation", '<', '>', true, |inner| {
let content = inner
.source
.trim();
if content.starts_with("https://") {
if content.contains(':') {
Ok(Target::Remote(External {
value: content,
span: inner.span_of(content),
Expand Down
12 changes: 7 additions & 5 deletions src/runner/checks/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,8 @@ fn step_outcomes_recorded() {
.is_empty()
})
.collect();
// Start + Begin + Done — three lines.
assert_eq!(lines.len(), 3);
// Start + Begin + Done + Finish — four lines.
assert_eq!(lines.len(), 4);
assert_eq!(
lines[0],
"2026-05-16T00:00:00Z 000001 / Start file:///tmp/Test.tq"
Expand Down Expand Up @@ -1411,8 +1411,9 @@ fn loop_inside_step_produces_one_result() {
.run(env)
.expect("run");

// One Start record, then the enclosing step's Begin and Done — the
// Loop inside the step body does not record events of its own.
// One Start record, then the enclosing step's Begin and Done, then the
// closing Finish — the Loop inside the step body does not record events of
// its own.
let pfftt = fixture.pfftt_contents();
let lines: Vec<&str> = pfftt
.lines()
Expand All @@ -1422,9 +1423,10 @@ fn loop_inside_step_produces_one_result() {
.is_empty()
})
.collect();
assert_eq!(lines.len(), 3);
assert_eq!(lines.len(), 4);
assert!(lines[1].ends_with(" Begin"));
assert!(lines[2].contains(" Done"));
assert!(lines[3].ends_with(" Finish"));
}

#[test]
Expand Down
17 changes: 17 additions & 0 deletions src/runner/checks/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,17 @@ fn format_record_pins_on_disk_text() {
"2026-05-17T00:28:25Z 015003 / Resume\n"
);

let record = Record {
recorded: "2026-05-17T00:28:30Z".to_string(),
run_id: RunId(15003),
path: "/".to_string(),
state: State::Finish,
};
assert_eq!(
format_record(&record),
"2026-05-17T00:28:30Z 015003 / Finish\n"
);

let record = Record {
recorded: "2026-05-17T00:28:30Z".to_string(),
run_id: RunId(15003),
Expand Down Expand Up @@ -487,6 +498,12 @@ fn record_round_trips_through_format_and_parse() {
uri: "file:///foo/Bar.tq".to_string(),
},
},
Record {
recorded: "2026-05-17T00:28:25Z".to_string(),
run_id: RunId(1),
path: "/".to_string(),
state: State::Finish,
},
Record {
recorded: "2026-05-17T00:28:25Z".to_string(),
run_id: RunId(1),
Expand Down
48 changes: 47 additions & 1 deletion src/runner/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@ pub trait Driver {
/// subroutine — with its Qualified Name (the `↘` marker).
fn enter(&mut self, qualified: &str);

/// Cross into this document on the way in: the `⇒` boundary line carrying
/// the document name, version, and run identifier (`/ NetworkProbe,1 000096`).
fn commence(&mut self, label: &str);

/// Cross out of this document on the way out: the `⇐` boundary line carrying
/// the run identifier (`/ 000096`).
fn conclude(&mut self, label: &str);

/// Display a line of formatted content at the left margin.
fn display(&mut self, content: &str);

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

fn commence(&mut self, label: &str) {
let renderer = self.renderer();
write_marker_line(&mut self.output, &format!("⇒ {}", label), renderer);
let _ = writeln!(self.output);
}

fn conclude(&mut self, label: &str) {
let renderer = self.renderer();
write_marker_line(&mut self.output, &format!("⇐ {}", label), renderer);
}

fn display(&mut self, content: &str) {
let _ = writeln!(self.output, "{}", content);
let _ = writeln!(self.output);
Expand Down Expand Up @@ -205,7 +224,7 @@ impl<W: Write> Driver for Console<W> {
}

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

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

fn commence(&mut self, label: &str) {
write_marker_line(&mut self.output, &format!("⇒ {}", label), self.renderer);
let _ = writeln!(self.output);
}

fn conclude(&mut self, label: &str) {
write_marker_line(&mut self.output, &format!("⇐ {}", label), self.renderer);
}

fn display(&mut self, content: &str) {
let _ = writeln!(self.output, "{}", content);
let _ = writeln!(self.output);
Expand Down Expand Up @@ -1338,6 +1366,16 @@ impl<D: Driver, W: Write> Driver for Transcript<D, W> {
.enter(qualified);
}

fn commence(&mut self, label: &str) {
self.inner
.commence(label);
}

fn conclude(&mut self, label: &str) {
self.inner
.conclude(label);
}

fn display(&mut self, content: &str) {
self.inner
.display(content);
Expand Down Expand Up @@ -1458,6 +1496,10 @@ impl Driver for Headless {

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

fn commence(&mut self, _label: &str) {}

fn conclude(&mut self, _label: &str) {}

fn display(&mut self, _content: &str) {}

fn announce(&mut self, _message: &str) {}
Expand Down Expand Up @@ -1607,6 +1649,10 @@ impl Driver for Mock {
});
}

fn commence(&mut self, _label: &str) {}

fn conclude(&mut self, _label: &str) {}

fn display(&mut self, content: &str) {
self.events
.push(Event::Display(content.to_string()));
Expand Down
24 changes: 20 additions & 4 deletions src/runner/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,14 @@ pub fn start<'i>(
let pfftt = construct_state_path(&run_dir, document);
let appender = Appender::open(pfftt, run_id)?;
let completed = HashMap::new();
let label = document_label(document);
let outcome = match mode {
Mode::Interactive => {
if !std::io::stdout().is_terminal() {
return Err(RunnerError::TerminalRequired);
}
let mut runner = Runner::new(program, appender, completed, Console::new(), library);
let mut runner = Runner::new(program, appender, completed, Console::new(), library)
.with_document(label);
runner.run(env)?
}
Mode::Automatic => {
Expand All @@ -63,7 +65,8 @@ pub fn start<'i>(
completed,
Automatic::new(colour),
library,
);
)
.with_document(label);
runner.run(env)?
}
};
Expand Down Expand Up @@ -133,8 +136,21 @@ pub fn resume<'i>(
state: State::Resume,
};
appender.append(&record)?;
let mut runner =
Runner::new(program, appender, completed, Console::new(), library).with_inputs(inputs);
let mut runner = Runner::new(program, appender, completed, Console::new(), library)
.with_inputs(inputs)
.with_document(document_label(&document));
let env = Environment::new();
runner.run(env)
}

// The boundary trace lines name the document by its file stem (`NetworkProbe`
// for `NetworkProbe.tq`), matching the PFFTT file the run writes to.
fn document_label(document: &Path) -> String {
document
.file_stem()
.map(|s| {
s.to_string_lossy()
.into_owned()
})
.unwrap_or_default()
}
64 changes: 62 additions & 2 deletions src/runner/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ pub struct Runner<'i, D: Driver> {
path: QualifiedPath<'i>,
library: Library,
context: Context,
document: Option<String>,
}

impl<'i, D: Driver> Runner<'i, D> {
Expand All @@ -132,9 +133,17 @@ impl<'i, D: Driver> Runner<'i, D> {
path: QualifiedPath::new(),
library,
context: Context::native(),
document: None,
}
}

/// Name the source document so the run brackets its walk double arrow
/// marked trace lines.
pub fn with_document(mut self, document: String) -> Self {
self.document = Some(document);
self
}

/// Seed the runner with the inputs recorded by a prior run — the values
/// supplied to the entry procedure and to each invocation — so a resume
/// restores them rather than re-prompting. Empty on a fresh run.
Expand Down Expand Up @@ -167,6 +176,17 @@ impl<'i, D: Driver> Runner<'i, D> {
/// anonymous wrapper if the document is top-level Steps, otherwise
/// the first declared procedure.
pub fn run(&mut self, mut env: Environment) -> Result<Outcome, RunnerError> {
if let Some(document) = &self.document {
let label = format!(
"/ {},1 #{}",
document,
self.appender
.run_id()
.render()
);
self.driver
.commence(&label);
}
if let Some(metadata) = self
.program
.prelude
Expand Down Expand Up @@ -259,6 +279,27 @@ impl<'i, D: Driver> Runner<'i, D> {
} else {
result
};
// A run that walked to its end closes with a `Finish` record at the
// root and the double arrow marker.
if let Ok(outcome) = &result {
if let Outcome::Stopped = outcome {
} else {
self.record_finish()?;
if self
.document
.is_some()
{
let label = format!(
"/ #{}",
self.appender
.run_id()
.render()
);
self.driver
.conclude(&label);
}
}
}
result
}

Expand Down Expand Up @@ -785,7 +826,7 @@ impl<'i, D: Driver> Runner<'i, D> {
}

self.driver
.settle("", &qualified, &input);
.settle("", &qualified, &input);
let outcome = outcome_from(input);
self.appender
.append(&Record {
Expand Down Expand Up @@ -839,11 +880,15 @@ impl<'i, D: Driver> Runner<'i, D> {
Some(text) => Some(text.as_str()),
None => None,
};
// Set the acquired `(name : forma)` off from the path with a
// trailing space; an invocation prompt instead glues its arguments
// straight to the `<callee>`.
let prompt = format!("{qualified} ");
let mut acquired = Vec::with_capacity(names.len());
for name in names {
match self
.driver
.acquire(&qualified, Some(name.value), forma)
.acquire(&prompt, Some(name.value), forma)
{
UserInput::Done(value) => acquired.push(value),
UserInput::Skip => {
Expand Down Expand Up @@ -1486,6 +1531,21 @@ impl<'i, D: Driver> Runner<'i, D> {
Ok(outcome)
}

/// Record a `Finish` at the root path, closing a run that walked to its end.
fn record_finish(&mut self) -> Result<(), RunnerError> {
let run_id = self
.appender
.run_id();
let record = Record {
recorded: now_iso8601(),
run_id,
path: "/".to_string(),
state: State::Finish,
};
self.appender
.append(&record)
}

/// Record a deliberate Stop at the root path and unwind the walk.
fn record_stop(&mut self) -> Result<Outcome, RunnerError> {
let run_id = self
Expand Down
Loading