From bc6cf219beaab49a355c3240c7432185337d17f7 Mon Sep 17 00:00:00 2001 From: Andrew Cowie Date: Tue, 30 Jun 2026 17:12:31 +1000 Subject: [PATCH 1/7] Add parsing error for mixed section content --- src/editor/server.rs | 4 +++ src/parsing/parser.rs | 2 ++ src/problem/messages.rs | 31 +++++++++++++++++++++ tests/broken/parsing/MixedSectionContent.tq | 19 +++++++++++++ 4 files changed, 56 insertions(+) create mode 100644 tests/broken/parsing/MixedSectionContent.tq diff --git a/src/editor/server.rs b/src/editor/server.rs index c17df3b6..c0613539 100644 --- a/src/editor/server.rs +++ b/src/editor/server.rs @@ -762,6 +762,10 @@ impl TechniqueLanguageServer { "Invalid section heading".to_string(), DiagnosticSeverity::ERROR, ), + ParsingError::MixedSectionContent(_) => ( + "Section content must be steps or procedures".to_string(), + DiagnosticSeverity::ERROR, + ), ParsingError::InvalidInvocation(_) => ( "Invalid procedure Invocation".to_string(), DiagnosticSeverity::ERROR, diff --git a/src/parsing/parser.rs b/src/parsing/parser.rs index 5a2e5a25..0f6852a4 100644 --- a/src/parsing/parser.rs +++ b/src/parsing/parser.rs @@ -43,6 +43,7 @@ pub enum ParsingError { InvalidParameters(Span), InvalidDeclaration(Span), InvalidSection(Span), + MixedSectionContent(Span), InvalidInvocation(Span), InvalidFunction(Span), InvalidCodeBlock(Span), @@ -77,6 +78,7 @@ impl ParsingError { | ParsingError::InvalidParameters(span) | ParsingError::InvalidDeclaration(span) | ParsingError::InvalidSection(span) + | ParsingError::MixedSectionContent(span) | ParsingError::InvalidInvocation(span) | ParsingError::InvalidFunction(span) | ParsingError::InvalidCodeBlock(span) diff --git a/src/problem/messages.rs b/src/problem/messages.rs index 201afb1e..dd8f4dd4 100644 --- a/src/problem/messages.rs +++ b/src/problem/messages.rs @@ -472,6 +472,37 @@ author of the Technique. .to_string(), ) } + ParsingError::MixedSectionContent(_) => ( + "Section mixes steps and procedures".to_string(), + r#" +A section contains either steps or procedures, but not both. A section that +begins with steps cannot then declare a procedure: + + I. Mix Drink + + 1. Take the juice from one bottle + 2. Pour in one measure of water + + prepare_mega_gin : + +Writing steps directly under a heading is a convenient shorthand and perfectly +valid Technique, but to add a helper procedure alongside them you will need to +upgrade to the main part of the section being a named procedure, and then you +can write a helper procedure that follows + + I. Mix Drink + + mix_drink : + + 1. Take the juice from one bottle + 2. Pour in one measure of water + 3. Allow three cubes to melt + + prepare_mega_gin : + "# + .trim_ascii() + .to_string(), + ), ParsingError::InvalidInvocation(_) => { let examples = vec![ Invocation { diff --git a/tests/broken/parsing/MixedSectionContent.tq b/tests/broken/parsing/MixedSectionContent.tq new file mode 100644 index 00000000..19d46e00 --- /dev/null +++ b/tests/broken/parsing/MixedSectionContent.tq @@ -0,0 +1,19 @@ +mix_drink : + +I. Mix Drink + + 1. Take the juice + 2. Pour in one measure + 3. Allow three cubes + +II. Enjoy + + 1. Drink + +III. Rehabilitation + + 1. Seek help + +prepare_gin : + 1. Remove cubes + 2. Drop them in From 947af12a00280eccf604ccbb3dac1e8958dda477 Mon Sep 17 00:00:00 2001 From: Andrew Cowie Date: Tue, 30 Jun 2026 17:41:26 +1000 Subject: [PATCH 2/7] Add more refined predicate for detecting procedures errant in sections --- src/parsing/parser.rs | 102 +++++++++++++++++++++++++++++------------- 1 file changed, 70 insertions(+), 32 deletions(-) diff --git a/src/parsing/parser.rs b/src/parsing/parser.rs index 0f6852a4..92244081 100644 --- a/src/parsing/parser.rs +++ b/src/parsing/parser.rs @@ -1346,6 +1346,18 @@ impl<'i> Parser<'i> { } else if is_step_parallel(outer.source) { let step = outer.read_step_parallel()?; steps.push(step); + } else if begins_procedure_declaration(outer.source) { + // A section that began with steps cannot then declare a + // procedure; the two cannot be mixed. + let line = outer + .source + .lines() + .next() + .unwrap_or(""); + return Err(ParsingError::MixedSectionContent(Span::new( + outer.offset, + line.len(), + ))); } else { // Skip unrecognized content line by line outer.skip_to_next_line(); @@ -2123,43 +2135,47 @@ impl<'i> Parser<'i> { /// Parse top-level ordered step fn read_step_dependent(&mut self) -> Result, ParsingError> { - self.take_block_lines(is_step_dependent, is_step_dependent, |outer| { - outer.trim_whitespace(); - let start = outer.offset; + self.take_block_lines( + is_step_dependent, + |line| is_step_dependent(line) || begins_procedure_declaration(line), + |outer| { + outer.trim_whitespace(); + let start = outer.offset; - // Parse ordinal - let re = regex!(r"^\s*(\d+)\.\s+"); - let cap = re - .captures(outer.source) - .ok_or(ParsingError::InvalidStep(Span::new(outer.offset, 0)))?; + // Parse ordinal + let re = regex!(r"^\s*(\d+)\.\s+"); + let cap = re + .captures(outer.source) + .ok_or(ParsingError::InvalidStep(Span::new(outer.offset, 0)))?; - let number = cap - .get(1) - .ok_or(ParsingError::Expected( - Span::new(outer.offset, 0), - "the ordinal Step number", - ))? - .as_str(); - - let l = cap - .get(0) - .unwrap() - .len(); + let number = cap + .get(1) + .ok_or(ParsingError::Expected( + Span::new(outer.offset, 0), + "the ordinal Step number", + ))? + .as_str(); - outer.advance(l); + let l = cap + .get(0) + .unwrap() + .len(); - let text = outer.read_descriptive()?; + outer.advance(l); - // Parse scopes (role assignments and substeps) - let scopes = outer.read_scopes()?; + let text = outer.read_descriptive()?; - return Ok(Scope::DependentBlock { - ordinal: number, - description: text, - subscopes: scopes, - span: outer.span_since(start), - }); - }) + // Parse scopes (role assignments and substeps) + let scopes = outer.read_scopes()?; + + return Ok(Scope::DependentBlock { + ordinal: number, + description: text, + subscopes: scopes, + span: outer.span_since(start), + }); + }, + ) } /// Parse a top-level concurrent step @@ -2829,7 +2845,6 @@ fn is_identifier(content: &str) -> bool { /// /// terminated by an end of line. -#[allow(unused)] fn is_signature(content: &str) -> bool { let re = regex!(r"\s*.+?\s*->\s*.+?\s*$"); @@ -3058,6 +3073,29 @@ fn is_procedure_declaration(content: &str) -> bool { } } +/// Tests whether a line begins a complete procedure declaration. Used as a +/// boundary when reading steps: unlike the permissive is_procedure_declaration(), +/// the text after the colon must be empty or a valid signature, so a descriptive +/// line such as "note: be careful here" is not mistaken for a declaration. +fn begins_procedure_declaration(content: &str) -> bool { + let line = content + .lines() + .next() + .unwrap_or(""); + + if !is_procedure_declaration(line) { + return false; + } + + match line.split_once(':') { + Some((_, after)) => { + let after = after.trim_ascii(); + after.is_empty() || is_signature(after) + } + None => false, + } +} + /// Detects any line that could potentially be a procedure declaration, /// including malformed ones. Used for detecting the end-boundary of a /// procedure. From 14c9c21b088c78e2bf6e447e72039ff52a89c003 Mon Sep 17 00:00:00 2001 From: Andrew Cowie Date: Wed, 1 Jul 2026 08:37:29 +1000 Subject: [PATCH 3/7] Fix Skip rollup --- src/runner/runner.rs | 45 ++++++++++++++++++++++------- tests/golden/runner/MeltCubes.pfftt | 15 ++++++++++ tests/golden/runner/MeltCubes.tq | 9 ++++++ 3 files changed, 59 insertions(+), 10 deletions(-) create mode 100644 tests/golden/runner/MeltCubes.pfftt create mode 100644 tests/golden/runner/MeltCubes.tq diff --git a/src/runner/runner.rs b/src/runner/runner.rs index a95e1792..540ef9fe 100644 --- a/src/runner/runner.rs +++ b/src/runner/runner.rs @@ -267,7 +267,7 @@ impl<'i, D: Driver> Runner<'i, D> { let qualified = self .path .render(); - let computable = computable(&entry.body); + let computable = is_scope_computable(&entry.body); let sealed = match result { Ok(Outcome::Stopped) => Ok(Outcome::Stopped), Ok(outcome) => self.seal_scope(&qualified, outcome, computable), @@ -787,7 +787,7 @@ impl<'i, D: Driver> Runner<'i, D> { // then sign off its scope; a Quit or error skips the // sign-off, leaving the procedure unfinished. let result = self.walk(&mut local, &subroutine.body); - let computable = computable(&subroutine.body); + let computable = is_scope_computable(&subroutine.body); let sealed = match result { Ok(Outcome::Stopped) => Ok(Outcome::Stopped), Ok(outcome) => self.seal_scope(&lexical, outcome, computable), @@ -1103,6 +1103,15 @@ impl<'i, D: Driver> Runner<'i, D> { } _ => self.walk(env, op)?, }; + // Pure descriptive prose carries a value but performs no work: it + // is neutral in the rollup, so an Invoke (or other work) that skips + // is not masked by the prose surrounding it in the same paragraph. + if let Operation::Prose(_) = op { + if let Outcome::Done(value) = outcome { + last = value; + } + continue; + } match outcome { Outcome::Done(value) => { done_seen = true; @@ -1159,7 +1168,7 @@ impl<'i, D: Driver> Runner<'i, D> { let result = self.perform_section(env, numeral, title, body); self.path .pop(); - let computable = computable(body); + let computable = is_scope_computable(body); // A section is a structural scope: the user signs it off at its // close before the next sibling runs. A Quit or error walk skips the // prompt — the section did not complete. @@ -1429,7 +1438,7 @@ impl<'i, D: Driver> Runner<'i, D> { .iter() .map(|r| r.value) .collect(); - let computable = computable(body); + let computable = is_step_computable(body); // `ask` consumes `produced`; keep a copy for a Skip to propagate. let propagate = produced.clone(); let input = self @@ -1725,17 +1734,33 @@ fn describe_execute(function: &str) -> String { format!("{}()", function) } -/// This is true when a body holds work to perform (that can be evaluated); a -/// step whose definition is purely descriptive prose can and will have a -/// Result, but is not computable. -fn computable(op: &Operation) -> bool { +/// Whether a scope holds any work to perform, scanning every member. If any +/// enclose steps or etc are computable then their results will subsequently +/// be rolled-up (`Fail` > `Done` > `Skip`). If not computable then (in +/// automatic) this will end up being judged `Skip`. +fn is_scope_computable(op: &Operation) -> bool { match op { // A pure-prose body is not computable (this is fine!). Operation::Sequence(ops) if ops.is_empty() => false, Operation::Sequence(ops) | Operation::Prologue(ops) => ops .iter() - .any(computable), - Operation::Step { body, .. } | Operation::Section { body, .. } => computable(body), + .any(is_scope_computable), + Operation::Step { body, .. } | Operation::Section { body, .. } => is_scope_computable(body), + Operation::Prose(_) => false, + _ => true, + } +} + +/// Whether a step's result is computed rather than being purely descriptive. +/// A procedure description of step content evaluates to its final member: a +/// step ending in an invocation, code block, or substep computes a result, +/// while one ending in descriptive prose does not. +fn is_step_computable(op: &Operation) -> bool { + match op { + Operation::Sequence(ops) | Operation::Prologue(ops) => match ops.last() { + Some(last) => is_step_computable(last), + None => false, + }, Operation::Prose(_) => false, _ => true, } diff --git a/tests/golden/runner/MeltCubes.pfftt b/tests/golden/runner/MeltCubes.pfftt new file mode 100644 index 00000000..d9ab8566 --- /dev/null +++ b/tests/golden/runner/MeltCubes.pfftt @@ -0,0 +1,15 @@ +2026-06-14T05:58:08.700Z 000001 / Start file://tests/samples/runner/MeltCubes.tq +2026-06-14T05:58:08.700Z 000001 /melt_cubes: Begin +2026-06-14T05:58:08.700Z 000001 /melt_cubes:/1 Begin +2026-06-14T05:58:08.700Z 000001 /melt_cubes:/1 Invoke https://example.com/Helper +2026-06-14T05:58:08.700Z 000001 /melt_cubes:/1/ Begin +2026-06-14T05:58:08.700Z 000001 /melt_cubes:/1/ Skip +2026-06-14T05:58:08.700Z 000001 /melt_cubes:/1 Done () +2026-06-14T05:58:08.700Z 000001 /melt_cubes:/2 Begin +2026-06-14T05:58:08.700Z 000001 /melt_cubes:/2 Execute exec() +2026-06-14T05:58:08.700Z 000001 /melt_cubes:/2 Return "" +2026-06-14T05:58:08.700Z 000001 /melt_cubes:/2 Done "" +2026-06-14T05:58:08.700Z 000001 /melt_cubes:/3 Begin +2026-06-14T05:58:08.700Z 000001 /melt_cubes:/3 Done () +2026-06-14T05:58:08.700Z 000001 /melt_cubes: Done () +2026-06-14T05:58:08.700Z 000001 / Finish diff --git a/tests/golden/runner/MeltCubes.tq b/tests/golden/runner/MeltCubes.tq new file mode 100644 index 00000000..c72d88b9 --- /dev/null +++ b/tests/golden/runner/MeltCubes.tq @@ -0,0 +1,9 @@ +% technique v1 + +melt_cubes : + +# Melt the cubes + + 1. Allow the cubes to melt . + 2. Stir well { exec("/bin/true") } + 3. Serve From bb6a0cc0fa57c4606a4e7bae866f2bc860e76017 Mon Sep 17 00:00:00 2001 From: Andrew Cowie Date: Wed, 1 Jul 2026 14:33:47 +1000 Subject: [PATCH 4/7] Correct golden PFFTT samples in runner tests --- src/runner/runner.rs | 6 ++ tests/golden/runner/DemolitionBeams.pfftt | 8 +-- tests/golden/runner/ExecutionInSteps.pfftt | 14 +++-- tests/golden/runner/ExecutionInSteps.tq | 7 ++- tests/golden/runner/InspectHatches.pfftt | 20 +++---- tests/golden/runner/MeltCubes.pfftt | 4 +- tests/golden/runner/PatioCleaning.pfftt | 70 +++++++++++----------- tests/runner/samples.rs | 15 +++-- 8 files changed, 79 insertions(+), 65 deletions(-) diff --git a/src/runner/runner.rs b/src/runner/runner.rs index 540ef9fe..698d2d5f 100644 --- a/src/runner/runner.rs +++ b/src/runner/runner.rs @@ -1028,6 +1028,12 @@ impl<'i, D: Driver> Runner<'i, D> { } let value = super::evaluator::evaluate(&self.library, &self.context, env, expr)?; let items = super::evaluator::coerce_to_list(value)?; + // Roll the iterations up worst-wins: a failure fails the + // loop; otherwise a single Done makes it Done; an all-skipped + // (non-empty) loop rolls up to Skip. + let mut done_seen = false; + let mut skip_seen = false; + let mut failed: Option = None; for (i, item) in items .into_iter() .enumerate() diff --git a/tests/golden/runner/DemolitionBeams.pfftt b/tests/golden/runner/DemolitionBeams.pfftt index a5c3a112..20a929b0 100644 --- a/tests/golden/runner/DemolitionBeams.pfftt +++ b/tests/golden/runner/DemolitionBeams.pfftt @@ -1,10 +1,10 @@ 2026-06-14T05:58:08.700Z 000001 / Start file://tests/samples/runner/DemolitionBeams.tq 2026-06-14T05:58:08.700Z 000001 /remove_planet: Begin 2026-06-14T05:58:08.700Z 000001 /remove_planet:/1 Begin -2026-06-14T05:58:08.700Z 000001 /remove_planet:/1 Done () +2026-06-14T05:58:08.700Z 000001 /remove_planet:/1 Skip 2026-06-14T05:58:08.700Z 000001 /remove_planet:/2 Begin -2026-06-14T05:58:08.700Z 000001 /remove_planet:/2 Done () +2026-06-14T05:58:08.700Z 000001 /remove_planet:/2 Skip 2026-06-14T05:58:08.700Z 000001 /remove_planet:/3 Begin -2026-06-14T05:58:08.700Z 000001 /remove_planet:/3 Done () -2026-06-14T05:58:08.700Z 000001 /remove_planet: Done () +2026-06-14T05:58:08.700Z 000001 /remove_planet:/3 Skip +2026-06-14T05:58:08.700Z 000001 /remove_planet: Skip 2026-06-14T05:58:08.700Z 000001 / Finish diff --git a/tests/golden/runner/ExecutionInSteps.pfftt b/tests/golden/runner/ExecutionInSteps.pfftt index 477cd770..88241a96 100644 --- a/tests/golden/runner/ExecutionInSteps.pfftt +++ b/tests/golden/runner/ExecutionInSteps.pfftt @@ -3,10 +3,14 @@ 2026-06-23T05:57:54.062Z 000088 /local_network:/0 Begin 2026-06-23T05:57:54.062Z 000088 /local_network:/0 Execute exec() 2026-06-23T05:57:55.457Z 000088 /local_network:/0 Return "/usr" -2026-06-23T05:57:55.457Z 000088 /local_network:/0 Done () +2026-06-23T05:57:55.457Z 000088 /local_network:/0 Skip 2026-06-23T05:57:55.457Z 000088 /local_network:/1 Begin 2026-06-23T05:57:55.457Z 000088 /local_network:/1 Execute exec() -2026-06-23T05:57:55.646Z 000088 /local_network:/1 Return "eth0 UP" -2026-06-23T05:57:55.807Z 000088 /local_network:/1 Done () -2026-06-23T05:57:55.972Z 000088 /local_network: Done () -2026-06-23T05:57:55.972Z 000088 / Finish +2026-06-23T05:57:55.646Z 000088 /local_network:/1 Return "Linux" +2026-06-23T05:57:55.646Z 000088 /local_network:/1 Done "Linux" +2026-06-23T05:58:55.457Z 000088 /local_network:/2 Begin +2026-06-23T05:58:55.457Z 000088 /local_network:/2 Execute exec() +2026-06-23T05:58:55.646Z 000088 /local_network:/2 Return "eth0 UP" +2026-06-23T05:58:55.807Z 000088 /local_network:/2 Skip +2026-06-23T05:58:55.972Z 000088 /local_network: Done () +2026-06-23T05:58:55.972Z 000088 / Finish diff --git a/tests/golden/runner/ExecutionInSteps.tq b/tests/golden/runner/ExecutionInSteps.tq index 2a161cc9..9c71b127 100644 --- a/tests/golden/runner/ExecutionInSteps.tq +++ b/tests/golden/runner/ExecutionInSteps.tq @@ -2,10 +2,11 @@ local_network : # Local Network Connectivity -Establish that the local network { exec("ls -d /usr") } environment is -functioning. +Establish that the disk is mounted { exec("ls -d /usr") } and the local +network environment is functioning. - 1. Check physical network interface { exec( + 1. Ensure the penguin state { exec("uname -s") } + 2. Check physical network interface { exec( ```bash echo "eth0 UP" ``` diff --git a/tests/golden/runner/InspectHatches.pfftt b/tests/golden/runner/InspectHatches.pfftt index a9fce99e..893fc74f 100644 --- a/tests/golden/runner/InspectHatches.pfftt +++ b/tests/golden/runner/InspectHatches.pfftt @@ -1,22 +1,22 @@ 2026-06-14T05:58:08.700Z 000001 / Start file://tests/samples/runner/InspectHatches.tq 2026-06-14T05:58:08.700Z 000001 /inspect_access_hatches: Begin 2026-06-14T05:58:08.700Z 000001 /inspect_access_hatches:/[1]/1 Begin -2026-06-14T05:58:08.700Z 000001 /inspect_access_hatches:/[1]/1 Done () +2026-06-14T05:58:08.700Z 000001 /inspect_access_hatches:/[1]/1 Skip 2026-06-14T05:58:08.700Z 000001 /inspect_access_hatches:/[1]/2 Begin -2026-06-14T05:58:08.700Z 000001 /inspect_access_hatches:/[1]/2 Done () +2026-06-14T05:58:08.700Z 000001 /inspect_access_hatches:/[1]/2 Skip 2026-06-14T05:58:08.700Z 000001 /inspect_access_hatches:/[1]/3 Begin -2026-06-14T05:58:08.700Z 000001 /inspect_access_hatches:/[1]/3 Done () +2026-06-14T05:58:08.700Z 000001 /inspect_access_hatches:/[1]/3 Skip 2026-06-14T05:58:08.700Z 000001 /inspect_access_hatches:/[2]/1 Begin -2026-06-14T05:58:08.700Z 000001 /inspect_access_hatches:/[2]/1 Done () +2026-06-14T05:58:08.700Z 000001 /inspect_access_hatches:/[2]/1 Skip 2026-06-14T05:58:08.700Z 000001 /inspect_access_hatches:/[2]/2 Begin -2026-06-14T05:58:08.700Z 000001 /inspect_access_hatches:/[2]/2 Done () +2026-06-14T05:58:08.700Z 000001 /inspect_access_hatches:/[2]/2 Skip 2026-06-14T05:58:08.700Z 000001 /inspect_access_hatches:/[2]/3 Begin -2026-06-14T05:58:08.700Z 000001 /inspect_access_hatches:/[2]/3 Done () +2026-06-14T05:58:08.700Z 000001 /inspect_access_hatches:/[2]/3 Skip 2026-06-14T05:58:08.700Z 000001 /inspect_access_hatches:/[3]/1 Begin -2026-06-14T05:58:08.700Z 000001 /inspect_access_hatches:/[3]/1 Done () +2026-06-14T05:58:08.700Z 000001 /inspect_access_hatches:/[3]/1 Skip 2026-06-14T05:58:08.700Z 000001 /inspect_access_hatches:/[3]/2 Begin -2026-06-14T05:58:08.700Z 000001 /inspect_access_hatches:/[3]/2 Done () +2026-06-14T05:58:08.700Z 000001 /inspect_access_hatches:/[3]/2 Skip 2026-06-14T05:58:08.700Z 000001 /inspect_access_hatches:/[3]/3 Begin -2026-06-14T05:58:08.700Z 000001 /inspect_access_hatches:/[3]/3 Done () -2026-06-14T05:58:08.700Z 000001 /inspect_access_hatches: Done () +2026-06-14T05:58:08.700Z 000001 /inspect_access_hatches:/[3]/3 Skip +2026-06-14T05:58:08.700Z 000001 /inspect_access_hatches: Skip 2026-06-14T05:58:08.700Z 000001 / Finish diff --git a/tests/golden/runner/MeltCubes.pfftt b/tests/golden/runner/MeltCubes.pfftt index d9ab8566..ec863ed5 100644 --- a/tests/golden/runner/MeltCubes.pfftt +++ b/tests/golden/runner/MeltCubes.pfftt @@ -4,12 +4,12 @@ 2026-06-14T05:58:08.700Z 000001 /melt_cubes:/1 Invoke https://example.com/Helper 2026-06-14T05:58:08.700Z 000001 /melt_cubes:/1/ Begin 2026-06-14T05:58:08.700Z 000001 /melt_cubes:/1/ Skip -2026-06-14T05:58:08.700Z 000001 /melt_cubes:/1 Done () +2026-06-14T05:58:08.700Z 000001 /melt_cubes:/1 Skip 2026-06-14T05:58:08.700Z 000001 /melt_cubes:/2 Begin 2026-06-14T05:58:08.700Z 000001 /melt_cubes:/2 Execute exec() 2026-06-14T05:58:08.700Z 000001 /melt_cubes:/2 Return "" 2026-06-14T05:58:08.700Z 000001 /melt_cubes:/2 Done "" 2026-06-14T05:58:08.700Z 000001 /melt_cubes:/3 Begin -2026-06-14T05:58:08.700Z 000001 /melt_cubes:/3 Done () +2026-06-14T05:58:08.700Z 000001 /melt_cubes:/3 Skip 2026-06-14T05:58:08.700Z 000001 /melt_cubes: Done () 2026-06-14T05:58:08.700Z 000001 / Finish diff --git a/tests/golden/runner/PatioCleaning.pfftt b/tests/golden/runner/PatioCleaning.pfftt index 89f0065d..66d503c3 100644 --- a/tests/golden/runner/PatioCleaning.pfftt +++ b/tests/golden/runner/PatioCleaning.pfftt @@ -5,71 +5,71 @@ 2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/I/setup_machine: Begin 2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/I/setup_machine:/1 Begin 2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/I/setup_machine:/1/a Begin -2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/I/setup_machine:/1/a Done () +2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/I/setup_machine:/1/a Skip 2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/I/setup_machine:/1/b Begin -2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/I/setup_machine:/1/b Done () -2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/I/setup_machine:/1 Done () +2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/I/setup_machine:/1/b Skip +2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/I/setup_machine:/1 Skip 2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/I/setup_machine:/2 Begin 2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/I/setup_machine:/2/c Begin -2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/I/setup_machine:/2/c Done () +2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/I/setup_machine:/2/c Skip 2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/I/setup_machine:/2/d Begin -2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/I/setup_machine:/2/d Done () +2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/I/setup_machine:/2/d Skip 2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/I/setup_machine:/2/e Begin -2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/I/setup_machine:/2/e Done () -2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/I/setup_machine:/2 Done () +2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/I/setup_machine:/2/e Skip +2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/I/setup_machine:/2 Skip 2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/I/setup_machine:/3 Begin 2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/I/setup_machine:/3/f Begin -2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/I/setup_machine:/3/f Done () +2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/I/setup_machine:/3/f Skip 2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/I/setup_machine:/3/g Begin -2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/I/setup_machine:/3/g Done () +2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/I/setup_machine:/3/g Skip 2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/I/setup_machine:/3/h Begin -2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/I/setup_machine:/3/h Done () -2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/I/setup_machine:/3 Done () +2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/I/setup_machine:/3/h Skip +2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/I/setup_machine:/3 Skip 2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/I/setup_machine:/4 Begin -2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/I/setup_machine:/4 Done () +2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/I/setup_machine:/4 Skip 2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/I/setup_machine:/5 Begin -2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/I/setup_machine:/5 Done () -2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/I/setup_machine: Done () -2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/I Done () +2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/I/setup_machine:/5 Skip +2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/I/setup_machine: Skip +2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/I Skip 2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/II Begin 2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/II Invoke clean_patio: 2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/II/clean_patio: Begin -2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/II/clean_patio: Done () -2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/II Done () +2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/II/clean_patio: Skip +2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/II Skip 2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III Begin 2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III Invoke tidy_up: 2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III/tidy_up: Begin 2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III/tidy_up:/1 Begin -2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III/tidy_up:/1 Done () +2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III/tidy_up:/1 Skip 2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III/tidy_up:/2 Begin -2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III/tidy_up:/2 Done () +2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III/tidy_up:/2 Skip 2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III/tidy_up:/3 Begin 2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III/tidy_up:/3/a Begin -2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III/tidy_up:/3/a Done () +2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III/tidy_up:/3/a Skip 2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III/tidy_up:/3/b Begin -2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III/tidy_up:/3/b Done () +2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III/tidy_up:/3/b Skip 2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III/tidy_up:/3/c Begin -2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III/tidy_up:/3/c Done () -2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III/tidy_up:/3 Done () +2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III/tidy_up:/3/c Skip +2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III/tidy_up:/3 Skip 2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III/tidy_up:/4 Begin 2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III/tidy_up:/4/d Begin -2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III/tidy_up:/4/d Done () +2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III/tidy_up:/4/d Skip 2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III/tidy_up:/4/e Begin -2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III/tidy_up:/4/e Done () -2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III/tidy_up:/4 Done () +2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III/tidy_up:/4/e Skip +2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III/tidy_up:/4 Skip 2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III/tidy_up:/5 Begin 2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III/tidy_up:/5/f Begin -2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III/tidy_up:/5/f Done () +2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III/tidy_up:/5/f Skip 2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III/tidy_up:/5/g Begin -2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III/tidy_up:/5/g Done () -2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III/tidy_up:/5 Done () +2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III/tidy_up:/5/g Skip +2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III/tidy_up:/5 Skip 2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III/tidy_up:/6 Begin 2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III/tidy_up:/6/h Begin -2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III/tidy_up:/6/h Done () +2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III/tidy_up:/6/h Skip 2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III/tidy_up:/6/j Begin -2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III/tidy_up:/6/j Done () -2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III/tidy_up:/6 Done () -2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III/tidy_up: Done () -2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III Done () -2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning: Done () +2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III/tidy_up:/6/j Skip +2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III/tidy_up:/6 Skip +2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III/tidy_up: Skip +2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning:/III Skip +2026-06-14T05:58:08.773Z 000002 /high_pressure_cleaning: Skip 2026-06-14T05:58:08.773Z 000002 / Finish diff --git a/tests/runner/samples.rs b/tests/runner/samples.rs index 0afbdc44..f2605170 100644 --- a/tests/runner/samples.rs +++ b/tests/runner/samples.rs @@ -113,11 +113,14 @@ fn ensure_run() { .skip(1) .collect(); - let finished = if let Outcome::Done(_) = outcome { - true - } else { - println!("File {:?} did not finish Done: {:?}", file, outcome); - false + // A pure-prose procedure legitimately finishes Skipped under the + // automatic driver; only a Failed or Stopped run is a test failure. + let finished = match outcome { + Outcome::Done(_) | Outcome::Skipped(_) => true, + _ => { + println!("File {:?} did not finish cleanly: {:?}", file, outcome); + false + } }; if !finished || recorded != expected { @@ -145,7 +148,7 @@ fn ensure_run() { if !failures.is_empty() { panic!( - "Sample runs must finish Done and match their expected trail, but {} files failed", + "Sample runs must complete, and must match expected results, but {} files failed", failures.len() ); } From cfbbdea7aee938f6e5dab3816277ee32b430d82d Mon Sep 17 00:00:00 2001 From: Andrew Cowie Date: Wed, 1 Jul 2026 18:45:21 +1000 Subject: [PATCH 5/7] Refactor into Interface composed of Output and Visual --- src/runner/checks/driver.rs | 10 +- src/runner/checks/runner.rs | 7 +- src/runner/driver.rs | 967 ++++++++++++++++++++++-------------- src/runner/runner.rs | 224 ++++++--- 4 files changed, 743 insertions(+), 465 deletions(-) diff --git a/src/runner/checks/driver.rs b/src/runner/checks/driver.rs index 5450bd89..e8802dc9 100644 --- a/src/runner/checks/driver.rs +++ b/src/runner/checks/driver.rs @@ -119,9 +119,9 @@ fn automatic_settles_done_when_computable_skip_otherwise() { fn automatic_settle_renders_verdict_glyph() { let mut output: Vec = Vec::new(); let mut p = Automatic::with_handle(&mut output); - p.settle("→", "/I/1", &UserInput::Done(Value::Unitus)); - p.settle("→", "/I/2", &UserInput::Skip); - p.settle("↙", "/I", &UserInput::Done(Value::Unitus)); + p.show_verdict("→", "/I/1", &UserInput::Done(Value::Unitus)); + p.show_verdict("→", "/I/2", &UserInput::Skip); + p.show_verdict("↙", "/I", &UserInput::Done(Value::Unitus)); let written = String::from_utf8(output).expect("utf8"); assert!(written.contains("→ I/1 ✓")); assert!(written.contains("→ I/2 ⊘")); @@ -132,8 +132,8 @@ fn automatic_settle_renders_verdict_glyph() { fn console_settle_writes_verdict_line() { let mut output: Vec = Vec::new(); let mut p = Console::with_output(&mut output); - p.settle("→", "/I/1", &UserInput::Done(Value::Unitus)); - p.settle("↙", "/I", &UserInput::Skip); + p.show_verdict("→", "/I/1", &UserInput::Done(Value::Unitus)); + p.show_verdict("↙", "/I", &UserInput::Skip); let written = String::from_utf8(output).expect("utf8"); assert!(written.contains("→ I/1")); assert!(written.contains("✓")); diff --git a/src/runner/checks/runner.rs b/src/runner/checks/runner.rs index 62a32bfd..33972a4c 100644 --- a/src/runner/checks/runner.rs +++ b/src/runner/checks/runner.rs @@ -1419,13 +1419,14 @@ Prepare the ground { exec("true") } before the steps. .filter(|r| r.path == "/check:/0") .map(|r| &r.state) .collect(); - // The /0 scope is bracketed Begin…Done with the exec's trace between. + // The exec runs (its trace between Begin and the verdict), but the prologue + // ends in prose so it settles Skip. let State::Begin = zero[0] else { panic!("expected Begin first at /check:/0, got {:?}", zero[0]); }; - let State::Done(_) = zero[zero.len() - 1] else { + let State::Skip = zero[zero.len() - 1] else { panic!( - "expected Done last at /check:/0, got {:?}", + "expected Skip last at /check:/0, got {:?}", zero[zero.len() - 1] ); }; diff --git a/src/runner/driver.rs b/src/runner/driver.rs index 5088457f..a6f08779 100644 --- a/src/runner/driver.rs +++ b/src/runner/driver.rs @@ -4,11 +4,11 @@ //! this is a human user or a program running non-interactively. //! The walker tells the driver what to show and then asks for the step's -//! outcome. `Console` drives a raw-mode terminal UI, presenting a step or -//! scope's returned Value as an editable candidate and reading the operator's -//! keystrokes; `Automatic` takes the body's returned Value with no human -//! intervention; `Mock` is for testing, recording what the walker tried to -//! show and returning canned answers. +//! outcome. A driver is assembled from two orthogonal axes: an `Output` +//! presentation policy (`Visual` renders a trace, `Silent` shows nothing) and +//! a `Verdict` input policy (`Interactive` reads the user's keystrokes, +//! `Batch` takes the body's value unattended). `Interface` composes the two. +//! `Mock` is for testing. use std::io::{self, Write}; @@ -52,10 +52,11 @@ pub enum UserInput { } /// The default verdict a node's rolled-up body leaves standing at its sign-off. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +/// Declaration order encodes verdict precedence: Fail beats Done beats Skip. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum Standing { - Done, Skip, + Done, Fail, } @@ -165,7 +166,7 @@ pub trait Driver { /// Render the settled verdict line for a step or scope close: `marker` /// (`→` step, `↙` scope close), Qualified Name, and the verdict's glyph. /// Quit renders nothing. - fn settle(&mut self, marker: &str, qualified: &str, verdict: &UserInput); + fn show_verdict(&mut self, marker: &str, qualified: &str, verdict: &UserInput); /// Obtain a value for a deferred input: `Done` supplies it, Skip / Fail /// abandon the call, Quit stops the run. @@ -176,59 +177,121 @@ pub trait Driver { fn renderer(&self) -> &'static dyn Render; } -/// Interactive console prompt in a terminal. `step` / `section` / `announce` -/// print in "cooked mode" (to use the old terminfo slang term for it); `ask` -/// switches to "raw mode" to read keystrokes. The default is confirmation: -/// `` completes the step, accepting the body's value intact. The -/// `` menu offers Skip / Fail / Quit and, for an editable scalar, Edit — -/// the one path to reshape the value. The keystroke logic lives in -/// `Interaction`; this is the terminal shell around it. -pub struct Console { - output: W, +/// The presentation axis: what a driver shows. `Visual` renders the trace to a +/// terminal; `Silent` shows nothing. The `show_*` methods draw the line a +/// `Batch` verdict still displays for a point it decides without prompting; +/// `surface` hands an interactive verdict the raw sink to prompt on. +pub trait Output { + fn step(&mut self, qualified: &str, description: &str, depth: usize); + fn enter(&mut self, qualified: &str); + fn commence(&mut self, label: &str); + fn conclude(&mut self, label: &str, verdict: &UserInput); + fn display(&mut self, content: &str); + fn announce(&mut self, message: &str); + fn section(&mut self, qualified: &str, numeral: &str, title: &str); + + /// The settled verdict line for a step or scope close. + fn show_verdict(&mut self, marker: &str, qualified: &str, verdict: &UserInput); + + /// The `$ script` line for an auto-run command. + fn show_command(&mut self, qualified: &str, script: &str); + + /// The `» verb label` line for an action. + fn show_action(&mut self, verb: &str, label: &str); + + /// The `⇒` marker line for an external departure. + fn show_depart(&mut self, qualified: &str); + + /// The raw sink an interactive prompt draws on; `Silent` discards. + fn surface(&mut self) -> &mut dyn Write; + + fn renderer(&self) -> &'static dyn Render; } -impl Console { - pub fn new() -> Self { - Console { - output: io::stdout(), - } - } +/// The input axis: what verdict a driver returns for each prompt. `Interactive` +/// reads the user's keystrokes off the `Output`'s surface; `Batch` decides +/// unattended. Each method is handed `&mut O` so an interactive policy can draw +/// its prompt on that surface. +pub trait Verdict { + fn ask( + &mut self, + out: &mut O, + qualified: &str, + choices: &[&str], + produced: Value, + computable: bool, + ) -> UserInput; + + fn seal( + &mut self, + out: &mut O, + qualified: &str, + produced: Value, + computable: bool, + ) -> UserInput; + + fn command(&mut self, out: &mut O, qualified: &str, script: &str) -> UserInput; + + fn action( + &mut self, + out: &mut O, + qualified: &str, + name: &str, + verb: &str, + label: &str, + ) -> UserInput; + + fn acquire( + &mut self, + out: &mut O, + qualified: &str, + name: Option<&str>, + forma: Option<&str>, + ) -> UserInput; + + fn depart(&mut self, out: &mut O, qualified: &str) -> UserInput; + + fn external(&mut self, out: &mut O, qualified: &str) -> UserInput; + + fn overrule( + &mut self, + out: &mut O, + qualified: &str, + marker: &str, + standing: Standing, + ) -> UserInput; } -#[cfg(test)] -impl Console { - pub fn with_output(output: W) -> Self { - Console { output } - } +/// A visual presentation to a terminal, rendering the trace shared by the +/// interactive and batch drivers. +pub struct Visual { + output: W, + renderer: &'static dyn Render, } -impl Driver for Console { +impl Output for Visual { fn step(&mut self, fqn: &str, description: &str, depth: usize) { - let renderer = self.renderer(); render_step( &mut self.output, &display_path(fqn), description, depth, - renderer, + self.renderer, ); } fn enter(&mut self, qualified: &str) { - let renderer = self.renderer(); - render_enter(&mut self.output, &display_path(qualified), renderer); + render_enter(&mut self.output, &display_path(qualified), self.renderer); let _ = writeln!(self.output); } fn commence(&mut self, label: &str) { - let renderer = self.renderer(); - write_marker_line(&mut self.output, &format!("⇒ {}", label), renderer); + write_marker_line(&mut self.output, &format!("⇒ {}", label), self.renderer); let _ = writeln!(self.output); } fn conclude(&mut self, label: &str, verdict: &UserInput) { - let renderer = self.renderer(); - render_conclude(&mut self.output, label, verdict, renderer); + render_conclude(&mut self.output, label, verdict, self.renderer); } fn display(&mut self, content: &str) { @@ -238,10 +301,9 @@ impl Driver for Console { fn section(&mut self, qualified: &str, numeral: &str, title: &str) { let qualified = display_path(qualified); - let renderer = self.renderer(); - write_marker_line(&mut self.output, &format!("↘ {}", qualified), renderer); + write_marker_line(&mut self.output, &format!("↘ {}", qualified), self.renderer); let _ = writeln!(self.output); - render_section(&mut self.output, numeral, title, renderer); + render_section(&mut self.output, numeral, title, self.renderer); let _ = writeln!(self.output); } @@ -249,78 +311,430 @@ impl Driver for Console { write_indented(&mut self.output, message); } - fn ask( + fn show_verdict(&mut self, marker: &str, qualified: &str, verdict: &UserInput) { + let qualified = display_path(qualified); + render_settle(&mut self.output, marker, &qualified, verdict, self.renderer); + let _ = self + .output + .flush(); + } + + fn show_command(&mut self, qualified: &str, script: &str) { + render_command( + &mut self.output, + &display_path(qualified), + script, + self.renderer, + ); + } + + fn show_action(&mut self, verb: &str, label: &str) { + let text = if label.is_empty() { + format!("» {}", verb) + } else { + format!("» {} {}", verb, label) + }; + write_indented(&mut self.output, &text); + } + + fn show_depart(&mut self, qualified: &str) { + write_marker_line( + &mut self.output, + &format!("⇒ {}", display_path(qualified)), + self.renderer, + ); + } + + fn surface(&mut self) -> &mut dyn Write { + &mut self.output + } + + fn renderer(&self) -> &'static dyn Render { + self.renderer + } +} + +/// The silent (no UI) presentation: every callback is a no-op and the surface +/// discards, so a `Technique` can run without a terminal, leaving only the +/// output from the executed child commands. +pub struct Silent { + discard: io::Sink, +} + +impl Output for Silent { + fn step(&mut self, _qualified: &str, _description: &str, _depth: usize) {} + fn enter(&mut self, _qualified: &str) {} + fn commence(&mut self, _label: &str) {} + fn conclude(&mut self, _label: &str, _verdict: &UserInput) {} + fn display(&mut self, _content: &str) {} + fn announce(&mut self, _message: &str) {} + fn section(&mut self, _qualified: &str, _numeral: &str, _title: &str) {} + fn show_verdict(&mut self, _marker: &str, _qualified: &str, _verdict: &UserInput) {} + fn show_command(&mut self, _qualified: &str, _script: &str) {} + fn show_action(&mut self, _verb: &str, _label: &str) {} + fn show_depart(&mut self, _qualified: &str) {} + + fn surface(&mut self) -> &mut dyn Write { + &mut self.discard + } + + fn renderer(&self) -> &'static dyn Render { + &Identity + } +} + +/// The interactive verdict policy: prompt the user on the output's surface +/// and read their keystrokes. The keystroke logic lives in `Interaction`. The +/// default is confirmation: `` completes the step, accepting the +/// default action or the body's current value intact; the `` menu offers +/// Skip, Fail, Quit, sometimes Override, and (for an editable scalar) Edit. +pub struct Interactive; + +impl Verdict for Interactive { + fn ask( &mut self, + out: &mut O, qualified: &str, choices: &[&str], produced: Value, _computable: bool, ) -> UserInput { - prompt(&mut self.output, qualified, "→", choices, produced) + prompt(out.surface(), qualified, "→", choices, produced) } - fn depart(&mut self, qualified: &str) -> UserInput { - let input = prompt(&mut self.output, qualified, "⇒", &[], Value::Unitus); + fn seal( + &mut self, + out: &mut O, + qualified: &str, + produced: Value, + _computable: bool, + ) -> UserInput { + prompt(out.surface(), qualified, "↙", &[], produced) + } + + fn command(&mut self, out: &mut O, qualified: &str, script: &str) -> UserInput { + prompt_command(out.surface(), qualified, script) + } + + fn action( + &mut self, + out: &mut O, + qualified: &str, + name: &str, + verb: &str, + label: &str, + ) -> UserInput { + prompt_action(out.surface(), qualified, name, verb, label) + } + + fn acquire( + &mut self, + out: &mut O, + qualified: &str, + name: Option<&str>, + forma: Option<&str>, + ) -> UserInput { + let label = format!( + "{}({} : {})", + display_path(qualified), + name.unwrap_or("?"), + forma.unwrap_or("?") + ); + prompt_acquire(out.surface(), &label, is_list_forma(forma)) + } + + fn depart(&mut self, out: &mut O, qualified: &str) -> UserInput { + let input = prompt(out.surface(), qualified, "⇒", &[], Value::Unitus); if let UserInput::Quit = input { } else { - let renderer = self.renderer(); - write_marker_line( - &mut self.output, - &format!("⇒ {}", display_path(qualified)), - renderer, - ); + out.show_depart(qualified); } input } - fn external(&mut self, qualified: &str) -> UserInput { - prompt(&mut self.output, qualified, "⇐", &[], Value::Unitus) - } + fn external(&mut self, out: &mut O, qualified: &str) -> UserInput { + prompt(out.surface(), qualified, "⇐", &[], Value::Unitus) + } + + fn overrule( + &mut self, + out: &mut O, + qualified: &str, + marker: &str, + standing: Standing, + ) -> UserInput { + prompt_overrule(out.surface(), qualified, marker, standing) + } +} + +/// The policy for running unattended (ie, not interactive): take each step +/// and scope's body value as the result, if available, otherwise Skip. +pub struct Batch; + +fn unattended(produced: Value, computable: bool) -> UserInput { + if computable { + UserInput::Done(produced) + } else { + UserInput::Skip + } +} + +impl Verdict for Batch { + fn ask( + &mut self, + _out: &mut O, + _qualified: &str, + _choices: &[&str], + produced: Value, + computable: bool, + ) -> UserInput { + unattended(produced, computable) + } + + fn seal( + &mut self, + _out: &mut O, + _qualified: &str, + produced: Value, + computable: bool, + ) -> UserInput { + unattended(produced, computable) + } + + fn command(&mut self, out: &mut O, qualified: &str, script: &str) -> UserInput { + out.show_command(qualified, script); + UserInput::Done(Value::Literali(script.to_string())) + } + + fn action( + &mut self, + out: &mut O, + _qualified: &str, + _name: &str, + verb: &str, + label: &str, + ) -> UserInput { + out.show_action(verb, label); + UserInput::Done(Value::Unitus) + } + + fn acquire( + &mut self, + _out: &mut O, + _qualified: &str, + _name: Option<&str>, + _forma: Option<&str>, + ) -> UserInput { + UserInput::Done(Value::Unitus) + } + + fn depart(&mut self, out: &mut O, qualified: &str) -> UserInput { + out.show_depart(qualified); + UserInput::Done(Value::Unitus) + } + + fn external(&mut self, _out: &mut O, _qualified: &str) -> UserInput { + UserInput::Skip + } + + fn overrule( + &mut self, + _out: &mut O, + _qualified: &str, + _marker: &str, + standing: Standing, + ) -> UserInput { + match standing { + Standing::Done => UserInput::Done(Value::Unitus), + Standing::Skip => UserInput::Skip, + Standing::Fail => UserInput::Fail(String::new()), + } + } +} + +/// A `Driver` built from an `Output` and a `Verdict`: presentation calls go +/// to the output, verdict calls go to the verdict policy, both of which are +/// then passed the output so it can prompt with them. +pub struct Interface { + out: O, + verdict: V, +} + +impl Driver for Interface { + fn step(&mut self, qualified: &str, description: &str, depth: usize) { + self.out + .step(qualified, description, depth); + } + + fn enter(&mut self, qualified: &str) { + self.out + .enter(qualified); + } + + fn commence(&mut self, label: &str) { + self.out + .commence(label); + } + + fn conclude(&mut self, label: &str, verdict: &UserInput) { + self.out + .conclude(label, verdict); + } + + fn display(&mut self, content: &str) { + self.out + .display(content); + } + + fn announce(&mut self, message: &str) { + self.out + .announce(message); + } + + fn section(&mut self, qualified: &str, numeral: &str, title: &str) { + self.out + .section(qualified, numeral, title); + } + + fn ask( + &mut self, + qualified: &str, + choices: &[&str], + produced: Value, + computable: bool, + ) -> UserInput { + self.verdict + .ask(&mut self.out, qualified, choices, produced, computable) + } + + fn depart(&mut self, qualified: &str) -> UserInput { + self.verdict + .depart(&mut self.out, qualified) + } + + fn external(&mut self, qualified: &str) -> UserInput { + self.verdict + .external(&mut self.out, qualified) + } + + fn command(&mut self, qualified: &str, script: &str) -> UserInput { + self.verdict + .command(&mut self.out, qualified, script) + } + + fn action(&mut self, qualified: &str, name: &str, verb: &str, label: &str) -> UserInput { + self.verdict + .action(&mut self.out, qualified, name, verb, label) + } + + fn seal(&mut self, qualified: &str, produced: Value, computable: bool) -> UserInput { + self.verdict + .seal(&mut self.out, qualified, produced, computable) + } + + fn overrule(&mut self, qualified: &str, marker: &str, standing: Standing) -> UserInput { + self.verdict + .overrule(&mut self.out, qualified, marker, standing) + } + + fn show_verdict(&mut self, marker: &str, qualified: &str, verdict: &UserInput) { + self.out + .show_verdict(marker, qualified, verdict); + } + + fn acquire(&mut self, qualified: &str, name: Option<&str>, forma: Option<&str>) -> UserInput { + self.verdict + .acquire(&mut self.out, qualified, name, forma) + } + + fn renderer(&self) -> &'static dyn Render { + self.out + .renderer() + } +} + +/// Represents the interactive console prompt in a terminal: renders the +/// output trace and reads the user's keystrokes. +pub type Console = Interface, Interactive>; + +/// Non-interactive driver that still renders the trace, then takes each +/// body's value (if any) as the result. +pub type Automatic = Interface, Batch>; + +/// Non-interactive driver with no UI output, leaving only executed child +/// process's output the only thing written to the terminal. +pub type Headless = Interface; - fn command(&mut self, qualified: &str, script: &str) -> UserInput { - prompt_command(&mut self.output, qualified, script) +impl Console { + pub fn new() -> Self { + Interface { + out: Visual { + output: io::stdout(), + renderer: &Terminal, + }, + verdict: Interactive, + } } +} - fn action(&mut self, qualified: &str, name: &str, verb: &str, label: &str) -> UserInput { - prompt_action(&mut self.output, qualified, name, verb, label) +#[cfg(test)] +impl Console { + pub fn with_output(output: W) -> Self { + Interface { + out: Visual { + output, + renderer: &Terminal, + }, + verdict: Interactive, + } } +} - fn seal(&mut self, qualified: &str, produced: Value, _computable: bool) -> UserInput { - prompt(&mut self.output, qualified, "↙", &[], produced) +impl Automatic { + pub fn new(colour: bool) -> Self { + Interface { + out: Visual { + output: io::stdout(), + renderer: if colour { &Terminal } else { &Identity }, + }, + verdict: Batch, + } } +} - fn overrule(&mut self, qualified: &str, marker: &str, standing: Standing) -> UserInput { - prompt_overrule(&mut self.output, qualified, marker, standing) +#[cfg(test)] +impl Automatic { + pub fn with_handle(output: W) -> Self { + Interface { + out: Visual { + output, + renderer: &Identity, + }, + verdict: Batch, + } } - fn settle(&mut self, marker: &str, qualified: &str, verdict: &UserInput) { - let qualified = display_path(qualified); - let renderer = self.renderer(); - render_settle(&mut self.output, marker, &qualified, verdict, renderer); - let _ = self + pub fn into_output(self) -> W { + self.out .output - .flush(); - } - - fn acquire(&mut self, qualified: &str, name: Option<&str>, forma: Option<&str>) -> UserInput { - let label = format!( - "{}({} : {})", - display_path(qualified), - name.unwrap_or("?"), - forma.unwrap_or("?") - ); - prompt_acquire(&mut self.output, &label, is_list_forma(forma)) } +} - fn renderer(&self) -> &'static dyn Render { - &Terminal +impl Headless { + pub fn new() -> Self { + Interface { + out: Silent { + discard: io::sink(), + }, + verdict: Batch, + } } } /// Run one interactive prompt and return the user's verdict, clearing the /// live `▶` row on settle. -fn prompt( - out: &mut W, +fn prompt( + mut out: &mut dyn Write, qualified: &str, settle: &str, choices: &[&str], @@ -330,13 +744,17 @@ fn prompt( let result = interact(out, Interaction::begin(choices, produced), |o, i| { draw(o, &qualified, settle, i) }); - let _ = queue!(out, cursor::MoveToColumn(0), Clear(ClearType::CurrentLine)); + let _ = queue!( + &mut out, + cursor::MoveToColumn(0), + Clear(ClearType::CurrentLine) + ); let _ = out.flush(); result } -fn prompt_overrule( - out: &mut W, +fn prompt_overrule( + mut out: &mut dyn Write, qualified: &str, marker: &str, standing: Standing, @@ -345,7 +763,11 @@ fn prompt_overrule( let result = interact(out, Interaction::overrule(standing), |o, i| { draw(o, &qualified, marker, i) }); - let _ = queue!(out, cursor::MoveToColumn(0), Clear(ClearType::CurrentLine)); + let _ = queue!( + &mut out, + cursor::MoveToColumn(0), + Clear(ClearType::CurrentLine) + ); let _ = out.flush(); result } @@ -354,8 +776,8 @@ fn prompt_overrule( /// — and settle on the user's verdict. On Done it leaves a compact dark-grey /// trace `» {path} {name}()`; Skip / Fail / Quit clear the line for the step's /// own settle to follow. -fn prompt_action( - out: &mut W, +fn prompt_action( + mut out: &mut dyn Write, qualified: &str, name: &str, verb: &str, @@ -365,7 +787,11 @@ fn prompt_action( let result = interact(out, Interaction::begin(&[], Value::Unitus), |o, i| { draw_action(o, &qualified, verb, label, i) }); - let _ = queue!(out, cursor::MoveToColumn(0), Clear(ClearType::CurrentLine)); + let _ = queue!( + &mut out, + cursor::MoveToColumn(0), + Clear(ClearType::CurrentLine) + ); if let UserInput::Done(_) = &result { let _ = writeln!(out, "{}", format!("» {} {}()", qualified, name).dark_grey()); } @@ -378,7 +804,7 @@ fn prompt_action( /// typing edits it in place, Esc opens the menu (Skip / Fail / Quit). On /// `Done` the live line is redrawn in grey with the interactive prompt marker /// becoming the '$', reminiscent of a shell. -fn prompt_command(out: &mut W, qualified: &str, script: &str) -> UserInput { +fn prompt_command(mut out: &mut dyn Write, qualified: &str, script: &str) -> UserInput { let qualified = display_path(qualified); // Seed the editable line let script = script.trim_end(); @@ -398,7 +824,11 @@ fn prompt_command(out: &mut W, qualified: &str, script: &str) -> UserI |o, i| draw(o, &qualified, "→", i), ); - let _ = queue!(out, cursor::MoveToColumn(0), Clear(ClearType::CurrentLine)); + let _ = queue!( + &mut out, + cursor::MoveToColumn(0), + Clear(ClearType::CurrentLine) + ); if let UserInput::Done(produced) = &result { let ran = if let Value::Literali(text) = produced { text.trim_end() @@ -414,7 +844,7 @@ fn prompt_command(out: &mut W, qualified: &str, script: &str) -> UserI /// Solicit a deferred input on the `▶` prompt line: `` accepts the /// empty default, typing overrides it; the `` menu and `` abandon /// the call. -fn prompt_acquire(out: &mut W, label: &str, list: bool) -> UserInput { +fn prompt_acquire(mut out: &mut dyn Write, label: &str, list: bool) -> UserInput { let field = edit(String::new(), Value::Literali(String::new()), list); let result = interact( out, @@ -426,18 +856,22 @@ fn prompt_acquire(out: &mut W, label: &str, list: bool) -> UserInput { }, |o, i| draw(o, label, "↘", i), ); - let _ = queue!(out, cursor::MoveToColumn(0), Clear(ClearType::CurrentLine)); + let _ = queue!( + &mut out, + cursor::MoveToColumn(0), + Clear(ClearType::CurrentLine) + ); let _ = out.flush(); result } /// Drive one raw-mode interaction to a settled `UserInput`, leaving the prompt -/// row cleared. Shared by the step/scope prompt and the exec command gate; the +/// row cleared. Shared by the step/scope prompt and the exec command prompt; the /// caller writes whatever record line it wants afterward. -fn interact( - out: &mut W, +fn interact( + mut out: &mut dyn Write, mut interaction: Interaction, - mut render: impl FnMut(&mut W, &Interaction) -> io::Result<()>, + mut render: impl FnMut(&mut dyn Write, &Interaction) -> io::Result<()>, ) -> UserInput { // The interactive path is guarded on stdout being a terminal before the // walk begins, so a raw-mode failure here is an unexpected terminal fault @@ -461,7 +895,7 @@ fn interact( } }; let _ = disable_raw_mode(); - let _ = queue!(out, cursor::Show); + let _ = queue!(&mut out, cursor::Show); let _ = out.flush(); result } @@ -1008,13 +1442,17 @@ impl Interaction { /// Layout: `{settle} {path} ▶ {content}` — the settle arrow and path are dark /// grey (matching the trace lines above), and ▶ is the shell-prompt character /// before the cursor/content area. -fn draw( - out: &mut W, +fn draw( + mut out: &mut dyn Write, qualified: &str, settle: &str, interaction: &Interaction, ) -> io::Result<()> { - queue!(out, cursor::MoveToColumn(0), Clear(ClearType::CurrentLine))?; + queue!( + &mut out, + cursor::MoveToColumn(0), + Clear(ClearType::CurrentLine) + )?; let prefix = prompt_prefix_width(qualified, settle); // The `▶` previews the verdict Enter will settle: blue for an ordinary Done, @@ -1045,18 +1483,22 @@ fn draw( /// Draw the read-only action prompt line: `» {path} {verb} {label} ▶`, the verb /// in light brown, the marker and path dark grey like the trace lines above. -fn draw_action( - out: &mut W, +fn draw_action( + mut out: &mut dyn Write, qualified: &str, verb: &str, label: &str, interaction: &Interaction, ) -> io::Result<()> { - queue!(out, cursor::MoveToColumn(0), Clear(ClearType::CurrentLine))?; + queue!( + &mut out, + cursor::MoveToColumn(0), + Clear(ClearType::CurrentLine) + )?; write!(out, "{} ", format!("» {}", qualified).dark_grey())?; - queue!(out, SetForegroundColor(LIGHT_BROWN))?; + queue!(&mut out, SetForegroundColor(LIGHT_BROWN))?; write!(out, "{}", verb)?; - queue!(out, ResetColor)?; + queue!(&mut out, ResetColor)?; if !label.is_empty() { write!(out, " {}", label)?; } @@ -1072,8 +1514,8 @@ fn draw_action( /// absolute columns from the line start; when the content is wider than the /// terminal they exceed its width and `place_cursor` resolves the wrap. Shared /// by the step/command prompt and the action prompt. -fn draw_tail( - out: &mut W, +fn draw_tail( + out: &mut dyn Write, interaction: &Interaction, prefix: u16, ) -> io::Result<(Option, u16)> { @@ -1147,15 +1589,15 @@ fn draw_tail( /// When the content wrapped, an absolute `MoveToColumn` past the right margin /// clamps to the edge, so the target column is resolved against the terminal /// width into a move back from the end instead. -fn place_cursor(out: &mut W, cursor_col: Option, end_col: u16) -> io::Result<()> { +fn place_cursor(mut out: &mut dyn Write, cursor_col: Option, end_col: u16) -> io::Result<()> { match cursor_col { None => { - queue!(out, cursor::Hide)?; + queue!(&mut out, cursor::Hide)?; } Some(target) if target >= end_col => { // The cursor belongs at the end of what was just written, which is // exactly where writing left it — moving would only fight the wrap. - queue!(out, cursor::Show)?; + queue!(&mut out, cursor::Show)?; } Some(target) => { let width = size() @@ -1171,11 +1613,11 @@ fn place_cursor(out: &mut W, cursor_col: Option, end_col: u16) -> end_col / width }; let up = end_row - target / width; - queue!(out, cursor::Show)?; + queue!(&mut out, cursor::Show)?; if up > 0 { - queue!(out, cursor::MoveUp(up))?; + queue!(&mut out, cursor::MoveUp(up))?; } - queue!(out, cursor::MoveToColumn(target % width))?; + queue!(&mut out, cursor::MoveToColumn(target % width))?; } } out.flush() @@ -1205,7 +1647,7 @@ const LIGHT_BROWN: Color = Color::Rgb { /// Render a horizontal row of Response options in the formatter's orange, the /// active one in reverse video. -fn render_choices(out: &mut W, choices: &[&str], active: usize) -> io::Result<()> { +fn render_choices(mut out: &mut dyn Write, choices: &[&str], active: usize) -> io::Result<()> { for (i, choice) in choices .iter() .enumerate() @@ -1213,19 +1655,19 @@ fn render_choices(out: &mut W, choices: &[&str], active: usize) -> io: if i > 0 { write!(out, " ")?; } - queue!(out, SetAttribute(Attribute::Bold))?; + queue!(&mut out, SetAttribute(Attribute::Bold))?; if i == active { queue!( - out, + &mut out, SetBackgroundColor(Color::White), SetForegroundColor(RESPONSE_ACTIVE) )?; write!(out, " {} ", choice)?; } else { - queue!(out, SetForegroundColor(RESPONSE))?; + queue!(&mut out, SetForegroundColor(RESPONSE))?; write!(out, " {} ", choice)?; } - queue!(out, ResetColor, SetAttribute(Attribute::Reset))?; + queue!(&mut out, ResetColor, SetAttribute(Attribute::Reset))?; } Ok(()) } @@ -1233,7 +1675,11 @@ fn render_choices(out: &mut W, choices: &[&str], active: usize) -> io: /// Render the Esc-menu: the active item in reverse video, a disabled item (a /// greyed `Edit`) dimmed, the rest plain. The active item is always enabled, so /// reverse and dim never apply to the same item. -fn render_menu(out: &mut W, interaction: &Interaction, active: usize) -> io::Result<()> { +fn render_menu( + mut out: &mut dyn Write, + interaction: &Interaction, + active: usize, +) -> io::Result<()> { for (i, item) in MENU .iter() .enumerate() @@ -1243,13 +1689,13 @@ fn render_menu(out: &mut W, interaction: &Interaction, active: usize) } let label = item.label(); if i == active { - queue!(out, SetAttribute(Attribute::Reverse))?; + queue!(&mut out, SetAttribute(Attribute::Reverse))?; write!(out, " {} ", label)?; - queue!(out, SetAttribute(Attribute::Reset))?; + queue!(&mut out, SetAttribute(Attribute::Reset))?; } else if !interaction.enabled(*item) { - queue!(out, SetAttribute(Attribute::Dim))?; + queue!(&mut out, SetAttribute(Attribute::Dim))?; write!(out, " {} ", label)?; - queue!(out, SetAttribute(Attribute::Reset))?; + queue!(&mut out, SetAttribute(Attribute::Reset))?; } else { write!(out, " {} ", label)?; } @@ -1351,153 +1797,6 @@ fn text_key(buffer: &mut String, cursor: &mut usize, code: KeyCode) -> bool { } } -/// No-operator driver: writes a trace of each step to its output and takes -/// the body's computed value as the step's outcome, running to completion -/// or first failure. A pure-prose step (empty body value) records (). -pub struct Automatic { - output: W, - renderer: &'static dyn Render, -} - -impl Automatic { - pub fn new(colour: bool) -> Self { - Automatic { - output: io::stdout(), - renderer: if colour { &Terminal } else { &Identity }, - } - } -} - -#[cfg(test)] -impl Automatic { - pub fn with_handle(output: W) -> Self { - Automatic { - output, - renderer: &Identity, - } - } - - pub fn into_output(self) -> W { - self.output - } -} - -impl Driver for Automatic { - fn step(&mut self, fqn: &str, description: &str, depth: usize) { - render_step( - &mut self.output, - &display_path(fqn), - description, - depth, - self.renderer, - ); - } - - fn enter(&mut self, qualified: &str) { - render_enter(&mut self.output, &display_path(qualified), self.renderer); - 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, verdict: &UserInput) { - render_conclude(&mut self.output, label, verdict, self.renderer); - } - - fn display(&mut self, content: &str) { - let _ = writeln!(self.output, "{}", content); - let _ = writeln!(self.output); - } - - fn section(&mut self, qualified: &str, numeral: &str, title: &str) { - let qualified = display_path(qualified); - write_marker_line(&mut self.output, &format!("↘ {}", qualified), self.renderer); - let _ = writeln!(self.output); - render_section(&mut self.output, numeral, title, self.renderer); - let _ = writeln!(self.output); - } - - fn announce(&mut self, message: &str) { - write_indented(&mut self.output, message); - } - - fn ask( - &mut self, - _qualified: &str, - _choices: &[&str], - produced: Value, - computable: bool, - ) -> UserInput { - if computable { - UserInput::Done(produced) - } else { - UserInput::Skip - } - } - - fn depart(&mut self, qualified: &str) -> UserInput { - write_marker_line( - &mut self.output, - &format!("⇒ {}", display_path(qualified)), - self.renderer, - ); - UserInput::Done(Value::Unitus) - } - - fn external(&mut self, _qualified: &str) -> UserInput { - UserInput::Skip - } - - fn command(&mut self, qualified: &str, script: &str) -> UserInput { - render_command( - &mut self.output, - &display_path(qualified), - script, - self.renderer, - ); - UserInput::Done(Value::Literali(script.to_string())) - } - - fn action(&mut self, _qualified: &str, _name: &str, verb: &str, label: &str) -> UserInput { - let text = if label.is_empty() { - format!("» {}", verb) - } else { - format!("» {} {}", verb, label) - }; - write_indented(&mut self.output, &text); - UserInput::Done(Value::Unitus) - } - - fn seal(&mut self, _qualified: &str, produced: Value, computable: bool) -> UserInput { - if computable { - UserInput::Done(produced) - } else { - UserInput::Skip - } - } - - fn settle(&mut self, marker: &str, qualified: &str, verdict: &UserInput) { - let qualified = display_path(qualified); - render_settle(&mut self.output, marker, &qualified, verdict, self.renderer); - } - - fn acquire( - &mut self, - _qualified: &str, - _name: Option<&str>, - _forma: Option<&str>, - ) -> UserInput { - UserInput::Done(Value::Unitus) - } - - fn renderer(&self) -> &'static dyn Render { - self.renderer - } -} - #[derive(Debug)] #[allow(dead_code)] // fields are read only via Debug enum Trace { @@ -1506,7 +1805,8 @@ enum Trace { }, Leave { path: String, - outcome: Disposition, + // None marks a Quit. + outcome: Option, result: Value, }, Execute { @@ -1524,13 +1824,14 @@ enum Trace { }, } -#[derive(Debug)] -#[allow(dead_code)] // read only via Debug -enum Disposition { - Done, - Skip, - Fail(String), - Stop, +/// The standing a settled verdict leaves, or `None` for a Quit. +fn disposition(input: &UserInput) -> Option { + match input { + UserInput::Done(_) | UserInput::Override => Some(Standing::Done), + UserInput::Skip => Some(Standing::Skip), + UserInput::Fail(_) => Some(Standing::Fail), + UserInput::Quit => None, + } } /// A driver for debugging that prints each value-bearing callback as a @@ -1555,15 +1856,9 @@ impl Transcript { } fn trace_outcome(&mut self, path: &str, produced: Value, outcome: &UserInput) { - let outcome = match outcome { - UserInput::Done(_) | UserInput::Override => Disposition::Done, - UserInput::Skip => Disposition::Skip, - UserInput::Fail(reason) => Disposition::Fail(reason.clone()), - UserInput::Quit => Disposition::Stop, - }; self.emit(Trace::Leave { path: path.to_string(), - outcome, + outcome: disposition(outcome), result: produced, }); } @@ -1669,9 +1964,9 @@ impl Driver for Transcript { outcome } - fn settle(&mut self, marker: &str, qualified: &str, verdict: &UserInput) { + fn show_verdict(&mut self, marker: &str, qualified: &str, verdict: &UserInput) { self.inner - .settle(marker, qualified, verdict); + .show_verdict(marker, qualified, verdict); } fn acquire(&mut self, qualified: &str, name: Option<&str>, forma: Option<&str>) -> UserInput { @@ -1698,88 +1993,6 @@ impl Driver for Transcript { } } -/// No-operator, no-output driver: takes each step and scope's computed value as -/// its result, emitting nothing, and counts the results it settles — one per -/// step and per structural-scope close. Lets a Technique be run without a -/// terminal, the result count read back from `results()`. -pub struct Headless { - results: usize, -} - -impl Headless { - pub fn new() -> Self { - Headless { results: 0 } - } - - pub fn results(&self) -> usize { - self.results - } -} - -impl Driver for Headless { - fn step(&mut self, _qualified: &str, _description: &str, _depth: usize) {} - - fn enter(&mut self, _qualified: &str) {} - - fn commence(&mut self, _label: &str) {} - - fn conclude(&mut self, _label: &str, _verdict: &UserInput) {} - - fn display(&mut self, _content: &str) {} - - fn announce(&mut self, _message: &str) {} - - fn ask( - &mut self, - _qualified: &str, - _choices: &[&str], - produced: Value, - _computable: bool, - ) -> UserInput { - self.results += 1; - UserInput::Done(produced) - } - - fn depart(&mut self, _qualified: &str) -> UserInput { - UserInput::Done(Value::Unitus) - } - - fn external(&mut self, _qualified: &str) -> UserInput { - self.results += 1; - UserInput::Skip - } - - fn command(&mut self, _qualified: &str, script: &str) -> UserInput { - UserInput::Done(Value::Literali(script.to_string())) - } - - fn action(&mut self, _qualified: &str, _name: &str, _verb: &str, _label: &str) -> UserInput { - UserInput::Done(Value::Unitus) - } - - fn section(&mut self, _qualified: &str, _numeral: &str, _title: &str) {} - - fn seal(&mut self, _qualified: &str, produced: Value, _computable: bool) -> UserInput { - self.results += 1; - UserInput::Done(produced) - } - - fn settle(&mut self, _marker: &str, _qualified: &str, _verdict: &UserInput) {} - - fn acquire( - &mut self, - _qualified: &str, - _name: Option<&str>, - _forma: Option<&str>, - ) -> UserInput { - UserInput::Done(Value::Unitus) - } - - fn renderer(&self) -> &'static dyn Render { - &Identity - } -} - /// Simulated prompt responses for test cases. Returns answers from a /// pre-loaded queue and records every announcement / step / section call so a /// test can assert what the walker tried to show. @@ -1937,10 +2150,10 @@ impl Driver for Mock { .expect("Mock::external called with no canned answers remaining") } - /// Records the command beat and auto-commands the run (`Done`) without - /// draining the answer queue — the exec gate is orthogonal to the step - /// verdicts a test drives. A test asserting gate behaviour inspects the - /// recorded `Command` event. + /// Records the command and auto-confirms it (`Done`) without draining the + /// answer queue — a command is orthogonal to the step verdicts a test + /// drives. A test asserting command behaviour inspects the recorded + /// `Command` event. fn command(&mut self, qualified: &str, script: &str) -> UserInput { self.events .push(Event::Command { @@ -1950,9 +2163,9 @@ impl Driver for Mock { UserInput::Done(Value::Unitus) } - /// Records the action beat and auto-confirms the run (`Done`) without - /// draining the answer queue — like `command`, the action gate is - /// orthogonal to the step verdicts a test drives. + /// Records the action and auto-confirms it (`Done`) without draining the + /// answer queue — like `command`, an action is orthogonal to the step + /// verdicts a test drives. fn action(&mut self, qualified: &str, name: &str, verb: &str, label: &str) -> UserInput { self.events .push(Event::Action { @@ -1990,7 +2203,7 @@ impl Driver for Mock { }) } - fn settle(&mut self, _marker: &str, _qualified: &str, _verdict: &UserInput) {} + fn show_verdict(&mut self, _marker: &str, _qualified: &str, _verdict: &UserInput) {} fn acquire(&mut self, _qualified: &str, name: Option<&str>, forma: Option<&str>) -> UserInput { self.events diff --git a/src/runner/runner.rs b/src/runner/runner.rs index 698d2d5f..41aae7e8 100644 --- a/src/runner/runner.rs +++ b/src/runner/runner.rs @@ -331,7 +331,7 @@ impl<'i, D: Driver> Runner<'i, D> { // supplies the parallel ordinal counter. A bare Step never reaches // `walk` directly. Operation::Step { .. } => { - unreachable!("a Step is always walked as a Sequence member") + unreachable!() // a Step is always walked as a Sequence member } Operation::Loop { names, over, body, .. @@ -406,7 +406,7 @@ impl<'i, D: Driver> Runner<'i, D> { UserInput::Skip => Ok(Outcome::Skipped(Value::Unitus)), UserInput::Fail(reason) => Ok(Outcome::Throw(Failure::Aborted(reason))), UserInput::Override => { - unreachable!("a command prompt never offers Override") + unreachable!() // a command prompt never offers Override } UserInput::Quit => self.record_stop(), } @@ -430,7 +430,7 @@ impl<'i, D: Driver> Runner<'i, D> { UserInput::Skip => Ok(Outcome::Skipped(Value::Unitus)), UserInput::Fail(reason) => Ok(Outcome::Throw(Failure::Aborted(reason))), UserInput::Override => { - unreachable!("an action prompt never offers Override") + unreachable!() // an action prompt never offers Override } UserInput::Quit => self.record_stop(), } @@ -869,7 +869,7 @@ impl<'i, D: Driver> Runner<'i, D> { } self.driver - .settle("⇐", &qualified, &input); + .show_verdict("⇐", &qualified, &input); let outcome = outcome_from(input); self.appender .append(&Record { @@ -947,7 +947,7 @@ impl<'i, D: Driver> Runner<'i, D> { UserInput::Fail(reason) => { return Ok(Outcome::Failed(Failure::Aborted(reason))) } - UserInput::Override => unreachable!("an acquire prompt never offers Override"), + UserInput::Override => unreachable!(), // an acquire prompt never offers Override UserInput::Quit => return self.record_stop(), } } @@ -1028,12 +1028,7 @@ impl<'i, D: Driver> Runner<'i, D> { } let value = super::evaluator::evaluate(&self.library, &self.context, env, expr)?; let items = super::evaluator::coerce_to_list(value)?; - // Roll the iterations up worst-wins: a failure fails the - // loop; otherwise a single Done makes it Done; an all-skipped - // (non-empty) loop rolls up to Skip. - let mut done_seen = false; - let mut skip_seen = false; - let mut failed: Option = None; + let mut rollup = Rollup::new(); for (i, item) in items .into_iter() .enumerate() @@ -1041,11 +1036,18 @@ impl<'i, D: Driver> Runner<'i, D> { super::evaluator::bind_names(env, names, item)?; let number = i + 1; - if let Outcome::Stopped = self.walk_iteration(env, names, number, body)? { - return Ok(Outcome::Stopped); + match self.walk_iteration(env, names, number, body)? { + Outcome::Stopped => return Ok(Outcome::Stopped), + Outcome::Throw(f) => return Ok(Outcome::Throw(f)), + other => rollup.absorb(other), } } - Ok(Outcome::Done(Value::Unitus)) + // A loop yields unit, so discard the rolled-up value while keeping its verdict. + match rollup.settle() { + Outcome::Done(_) => Ok(Outcome::Done(Value::Unitus)), + Outcome::Skipped(_) => Ok(Outcome::Skipped(Value::Unitus)), + other => Ok(other), + } } } } @@ -1078,7 +1080,7 @@ impl<'i, D: Driver> Runner<'i, D> { }; if let Some(verdict) = verdict { self.driver - .settle("↙", &qualified, &verdict); + .show_verdict("↙", &qualified, &verdict); } self.path .pop(); @@ -1091,10 +1093,7 @@ impl<'i, D: Driver> Runner<'i, D> { ops: &'i [Operation<'i>], ) -> Result { let mut parallel_idx: usize = 0; - let mut last = Value::Unitus; - let mut failed: Option = None; - let mut done_seen = false; - let mut skip_seen = false; + let mut rollup = Rollup::new(); for op in ops { let outcome = match op { Operation::Step { ordinal, .. } => { @@ -1109,45 +1108,22 @@ impl<'i, D: Driver> Runner<'i, D> { } _ => self.walk(env, op)?, }; - // Pure descriptive prose carries a value but performs no work: it - // is neutral in the rollup, so an Invoke (or other work) that skips - // is not masked by the prose surrounding it in the same paragraph. + // A prose child contributes its value but no verdict, so it cannot + // make an otherwise-skipped sequence roll up as Done. if let Operation::Prose(_) = op { if let Outcome::Done(value) = outcome { - last = value; + rollup.observe(value); } continue; } match outcome { - Outcome::Done(value) => { - done_seen = true; - last = value; - } - Outcome::Skipped(value) => { - skip_seen = true; - last = value; - } + // Stopped and Throw abandon the sequence at once; a Fail rolls up. Outcome::Stopped => return Ok(Outcome::Stopped), - // A Throw is a hard failure mid-body; it propagates up to the - // enclosing step at once. A Fail is a step's recorded verdict; the - // sequence runs on to its siblings but carries the failure so the - // enclosing scope rolls up as failed (its sign-off may overrule). Outcome::Throw(failure) => return Ok(Outcome::Throw(failure)), - Outcome::Failed(failure) => { - if failed.is_none() { - failed = Some(failure); - } - } + other => rollup.absorb(other), } } - // Roll the children up by worst-wins precedence: any failure fails the - // sequence; otherwise a single green makes it Done; only an all-skipped - // (non-empty) sequence rolls up to Skip. - match failed { - Some(failure) => Ok(Outcome::Failed(failure)), - None if !done_seen && skip_seen => Ok(Outcome::Skipped(last)), - None => Ok(Outcome::Done(last)), - } + Ok(rollup.settle()) } fn walk_section( @@ -1216,13 +1192,11 @@ impl<'i, D: Driver> Runner<'i, D> { let Operation::Step { ordinal, attributes, - source, - body, - responses, .. } = op else { - unreachable!("walk_step called with non-Step operation"); + // walk_step called with non-Step operation + unreachable!(); }; for frame in attributes { @@ -1239,7 +1213,7 @@ impl<'i, D: Driver> Runner<'i, D> { .path .render(); - let result = self.perform_step(env, &qualified, ordinal, body, source, responses); + let result = self.perform_step(env, &qualified, op); self.path .pop(); @@ -1288,6 +1262,15 @@ impl<'i, D: Driver> Runner<'i, D> { if let Outcome::Stopped = outcome { return Ok(outcome); } + // A prologue takes a step's verdict rule: ending in prose, it settles + // Skip even when a command within it ran. + let computable = ops + .last() + .is_some_and(is_step_computable); + let outcome = match outcome { + Outcome::Done(value) if !computable => Outcome::Skipped(value), + other => other, + }; let record = Record { recorded: now_iso8601(), run_id: self @@ -1305,11 +1288,18 @@ impl<'i, D: Driver> Runner<'i, D> { &mut self, env: &mut Environment, qualified: &str, - _ordinal: &Ordinal<'i>, - body: &'i Operation<'i>, - source: &'i language::Scope<'i>, - responses: &[&'i language::Response<'i>], + op: &'i Operation<'i>, ) -> Result { + let Operation::Step { + source, + body, + responses, + .. + } = op + else { + // perform_step called with non-Step operation + unreachable!(); + }; if let Some(value) = self .completed .get(qualified) @@ -1380,7 +1370,7 @@ impl<'i, D: Driver> Runner<'i, D> { return self.record_stop(); } self.driver - .settle("→", qualified, &input); + .show_verdict("→", qualified, &input); let outcome = outcome_from(input); let record = Record { recorded: now_iso8601(), @@ -1413,10 +1403,11 @@ impl<'i, D: Driver> Runner<'i, D> { Outcome::Failed(Failure::Aborted(reason)) => { UserInput::Fail(reason.clone()) } - _ => unreachable!("only Skip and Fail reach the settled branch"), + // only Skip and Fail reach the settled branch + _ => unreachable!(), }; self.driver - .settle("→", qualified, &verdict); + .show_verdict("→", qualified, &verdict); return Ok(settled); } } @@ -1428,7 +1419,7 @@ impl<'i, D: Driver> Runner<'i, D> { if responses.is_empty() && binds_descriptively(body) { let outcome = Outcome::Done(produced); self.driver - .settle("→", qualified, &verdict_from(&outcome)); + .show_verdict("→", qualified, &verdict_from(&outcome)); let record = Record { recorded: now_iso8601(), run_id, @@ -1444,7 +1435,7 @@ impl<'i, D: Driver> Runner<'i, D> { .iter() .map(|r| r.value) .collect(); - let computable = is_step_computable(body); + let computable = is_step_computable(op); // `ask` consumes `produced`; keep a copy for a Skip to propagate. let propagate = produced.clone(); let input = self @@ -1458,7 +1449,7 @@ impl<'i, D: Driver> Runner<'i, D> { } self.driver - .settle("→", qualified, &input); + .show_verdict("→", qualified, &input); let outcome = match input { UserInput::Skip => Outcome::Skipped(propagate), other => outcome_from(other), @@ -1639,7 +1630,7 @@ impl<'i, D: Driver> Runner<'i, D> { return self.record_stop(); } self.driver - .settle("↙", qualified, &input); + .show_verdict("↙", qualified, &input); let settled = outcome_from(input); let record = Record { recorded: now_iso8601(), @@ -1668,7 +1659,7 @@ impl<'i, D: Driver> Runner<'i, D> { return self.record_stop(); } self.driver - .settle("↙", qualified, &input); + .show_verdict("↙", qualified, &input); let outcome = match input { UserInput::Skip => Outcome::Skipped(propagate), other => outcome_from(other), @@ -1740,35 +1731,110 @@ fn describe_execute(function: &str) -> String { format!("{}()", function) } -/// Whether a scope holds any work to perform, scanning every member. If any -/// enclose steps or etc are computable then their results will subsequently -/// be rolled-up (`Fail` > `Done` > `Skip`). If not computable then (in -/// automatic) this will end up being judged `Skip`. +/// Accumulates child outcomes into one worst-wins verdict: the rank climbs by +/// `Standing` precedence (Fail > Done > Skip) while the value tracks last-seen. +/// A `None` rank distinguishes an empty group (settles Done) from an all-skipped +/// one (settles Skip). +struct Rollup { + rank: Option, + value: Value, + failure: Option, +} + +impl Rollup { + fn new() -> Self { + Rollup { + rank: None, + value: Value::Unitus, + failure: None, + } + } + + fn absorb(&mut self, outcome: Outcome) { + let rank = match outcome { + Outcome::Done(value) => { + self.value = value; + Standing::Done + } + Outcome::Skipped(value) => { + self.value = value; + Standing::Skip + } + Outcome::Failed(failure) => { + if self + .failure + .is_none() + { + self.failure = Some(failure); + } + Standing::Fail + } + // control-flow outcomes short-circuit before roll-up + Outcome::Throw(_) | Outcome::Stopped => unreachable!(), + }; + self.rank = Some(match self.rank { + Some(cur) => cur.max(rank), + None => rank, + }); + } + + /// Prose contributes its value but not a verdict, so surrounding prose never + /// masks a skipped Invoke. + fn observe(&mut self, value: Value) { + self.value = value; + } + + fn settle(self) -> Outcome { + match self + .rank + .unwrap_or(Standing::Done) + { + Standing::Fail => Outcome::Failed( + self.failure + .unwrap(), + ), + Standing::Skip => Outcome::Skipped(self.value), + Standing::Done => Outcome::Done(self.value), + } + } +} + +/// Whether a scope holds any computable work, scanning every member. A scope of +/// pure prose is not computable, so an automatic run skips its sign-off. fn is_scope_computable(op: &Operation) -> bool { match op { - // A pure-prose body is not computable (this is fine!). - Operation::Sequence(ops) if ops.is_empty() => false, Operation::Sequence(ops) | Operation::Prologue(ops) => ops .iter() .any(is_scope_computable), Operation::Step { body, .. } | Operation::Section { body, .. } => is_scope_computable(body), + other => computes(other), + } +} + +/// A single operation's own computability, ignoring nesting: descriptive prose +/// computes nothing; every other primitive (invoke, execute, code, bind, ...) +/// does. The one leaf definition both fold predicates rest on. +fn computes(op: &Operation) -> bool { + match op { Operation::Prose(_) => false, _ => true, } } -/// Whether a step's result is computed rather than being purely descriptive. -/// A procedure description of step content evaluates to its final member: a -/// step ending in an invocation, code block, or substep computes a result, -/// while one ending in descriptive prose does not. +/// Whether a step computes its result rather than describing it. A step's value +/// is its final member, so one ending in an invocation, code, or substep +/// computes a value while one ending in prose does not. fn is_step_computable(op: &Operation) -> bool { match op { + // A step offering response choices needs a selection no non-interactive + // run can make; it is not computable and so is skipped. + Operation::Step { responses, .. } if !responses.is_empty() => false, + Operation::Step { body, .. } => is_step_computable(body), Operation::Sequence(ops) | Operation::Prologue(ops) => match ops.last() { Some(last) => is_step_computable(last), None => false, }, - Operation::Prose(_) => false, - _ => true, + other => computes(other), } } @@ -1814,9 +1880,7 @@ fn record_state(outcome: &Outcome) -> State { State::Fail(Some(super::state::fail_reason(reason))) } } - Outcome::Stopped => { - unreachable!("Stop is recorded as a lifecycle event, not a step result") - } + Outcome::Stopped => unreachable!(), // Stop is recorded as a lifecycle event, not a step result } } From 7bbe19059bfec6e2be185eb8569fbaa8046a88e2 Mon Sep 17 00:00:00 2001 From: Andrew Cowie Date: Wed, 1 Jul 2026 21:19:49 +1000 Subject: [PATCH 6/7] Refine Outcome to represent results and introduce Conclusion to signal --- src/main.rs | 10 +- src/runner/checks/driver.rs | 65 +++---- src/runner/checks/runner.rs | 34 ++-- src/runner/driver.rs | 58 +++--- src/runner/mod.rs | 8 +- src/runner/runner.rs | 377 +++++++++++++++++++----------------- tests/runner/samples.rs | 10 +- 7 files changed, 296 insertions(+), 266 deletions(-) diff --git a/src/main.rs b/src/main.rs index e8f77ea4..05d8bc47 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,7 +13,7 @@ use technique::highlighting::{self, Terminal}; use technique::linking; use technique::parsing; use technique::resolution; -use technique::runner::{self, Builtin, Library, Mode, Outcome, RunId}; +use technique::runner::{self, Builtin, Conclusion, Library, Mode, Outcome, RunId}; use technique::templating::{self, Checklist, NasaEsaIss, Procedure, Recipe, Source}; use technique::translation; @@ -960,14 +960,16 @@ fn main() { match runner::start( mode, colour, filename, &program, &arguments, library, &names, ) { - Ok((run_id, Outcome::Stopped)) => { + Ok((run_id, Conclusion::Stopping)) => { eprintln!( "stopped; resume with `technique resume {}`", run_id.render() ); std::process::exit(0); } - Ok((_, Outcome::Failed(_) | Outcome::Throw(_))) => std::process::exit(1), + Ok((_, Conclusion::Completed(Outcome::Fail(_)) | Conclusion::Throwing(_))) => { + std::process::exit(1) + } Ok((_, _)) => std::process::exit(0), Err(error) => { eprintln!("{}", problem::concise_runner_error(&error, &Terminal)); @@ -1084,7 +1086,7 @@ fn main() { } match runner::resume(run_id, &program, library) { - Ok(Outcome::Stopped) => { + Ok(Conclusion::Stopping) => { eprintln!( "stopped; continue with `technique resume {}`", run_id.render() diff --git a/src/runner/checks/driver.rs b/src/runner/checks/driver.rs index e8802dc9..46be80a2 100644 --- a/src/runner/checks/driver.rs +++ b/src/runner/checks/driver.rs @@ -1,8 +1,7 @@ use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use crate::runner::driver::{ - draw, edit, is_list_forma, Automatic, Console, Driver, Event, Interaction, Mock, Standing, - UserInput, + draw, edit, is_list_forma, Automatic, Console, Driver, Event, Mock, Prompt, Standing, UserInput, }; use crate::value::{Numeric, Value}; @@ -154,7 +153,7 @@ fn console_enter_writes_fqn() { fn default_enter_completes_with_produced() { // The default is confirmation: Enter accepts the body's value intact, so // an untouched Unitus stays Unitus, not Literali(""). - let mut it = Interaction::begin(&[], Value::Unitus); + let mut it = Prompt::begin(&[], Value::Unitus); assert_eq!( it.handle(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)), Some(UserInput::Done(Value::Unitus)) @@ -165,7 +164,7 @@ fn default_enter_completes_with_produced() { fn overrule_fail_enter_propagates() { // At a failed sign-off the default is the failure itself: a bare Enter // settles Fail and never silently lifts it. - let mut it = Interaction::overrule(Standing::Fail); + let mut it = Prompt::overrule(Standing::Fail); assert_eq!( it.handle(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)), Some(UserInput::Fail(String::new())) @@ -176,7 +175,7 @@ fn overrule_fail_enter_propagates() { fn overrule_fail_menu_o_overrides() { // Override is reachable only deliberately — from the menu — and settles as // Override, which the runner lifts to a rollup-severing Done. - let mut it = Interaction::overrule(Standing::Fail); + let mut it = Prompt::overrule(Standing::Fail); it.handle(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); assert_eq!( it.handle(KeyEvent::new(KeyCode::Char('o'), KeyModifiers::NONE)), @@ -187,7 +186,7 @@ fn overrule_fail_menu_o_overrides() { #[test] fn overrule_skip_enter_propagates() { // An all-skipped scope defaults to Skip, not Done. - let mut it = Interaction::overrule(Standing::Skip); + let mut it = Prompt::overrule(Standing::Skip); assert_eq!( it.handle(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)), Some(UserInput::Skip) @@ -198,7 +197,7 @@ fn overrule_skip_enter_propagates() { fn override_inert_without_a_failure() { // A Skip standing has nothing to override, so the menu's `o` is greyed and // does nothing. - let mut it = Interaction::overrule(Standing::Skip); + let mut it = Prompt::overrule(Standing::Skip); it.handle(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); assert_eq!( it.handle(KeyEvent::new(KeyCode::Char('o'), KeyModifiers::NONE)), @@ -210,7 +209,7 @@ fn override_inert_without_a_failure() { fn esc_edit_typed_enter_returns_literali() { // Editing is opt-in: Esc -> Edit (the first menu item) opens the buffer // seeded from the value, which the operator can then extend. - let mut it = Interaction::begin(&[], Value::Literali("eth".to_string())); + let mut it = Prompt::begin(&[], Value::Literali("eth".to_string())); it.handle(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); assert_eq!( it.handle(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)), @@ -229,7 +228,7 @@ fn esc_edit_typed_enter_returns_literali() { #[test] fn esc_edit_seeds_buffer_and_backspace_trims() { // Edit seeds the buffer from the produced value, cursor at the end. - let mut it = Interaction::begin(&[], Value::Literali("abc".to_string())); + let mut it = Prompt::begin(&[], Value::Literali("abc".to_string())); it.handle(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); it.handle(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); assert_eq!( @@ -248,7 +247,7 @@ fn quanticle_edit_roundtrips() { // Editing a numeric value and changing it keeps it numeric: 42 -> 43 is // re-parsed back to a Quanticle, not flattened to text. - let mut it = Interaction::begin(&[], quanticle()); + let mut it = Prompt::begin(&[], quanticle()); it.handle(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); it.handle(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); it.handle(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE)); @@ -263,7 +262,7 @@ fn quanticle_edit_roundtrips() { // Entering and leaving the edit without a change returns the original // numeric value untouched. - let mut it = Interaction::begin(&[], quanticle()); + let mut it = Prompt::begin(&[], quanticle()); it.handle(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); it.handle(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); assert_eq!( @@ -273,7 +272,7 @@ fn quanticle_edit_roundtrips() { // A numeric value edited into something that is not a number is not // accepted: Enter stays in the edit so it can be corrected. - let mut it = Interaction::begin(&[], quanticle()); + let mut it = Prompt::begin(&[], quanticle()); it.handle(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); it.handle(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); it.handle(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE)); @@ -288,7 +287,7 @@ fn esc_menu_navigates_edit_skip_fail_quit() { // For an editable scalar the menu is edit, skip, fail, quit in order. let editable = || Value::Literali("eth0".to_string()); - let mut it = Interaction::begin(&[], editable()); + let mut it = Prompt::begin(&[], editable()); it.handle(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); it.handle(KeyEvent::new(KeyCode::Right, KeyModifiers::NONE)); assert_eq!( @@ -296,7 +295,7 @@ fn esc_menu_navigates_edit_skip_fail_quit() { Some(UserInput::Skip) ); - let mut it = Interaction::begin(&[], editable()); + let mut it = Prompt::begin(&[], editable()); it.handle(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); it.handle(KeyEvent::new(KeyCode::Right, KeyModifiers::NONE)); it.handle(KeyEvent::new(KeyCode::Right, KeyModifiers::NONE)); @@ -312,7 +311,7 @@ fn esc_menu_navigates_edit_skip_fail_quit() { Some(UserInput::Fail("no".to_string())) ); - let mut it = Interaction::begin(&[], editable()); + let mut it = Prompt::begin(&[], editable()); it.handle(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); // Right past the end clamps on quit. for _ in 0..5 { @@ -328,7 +327,7 @@ fn esc_menu_navigates_edit_skip_fail_quit() { fn esc_menu_disables_edit_for_unit_and_complex() { // Neither a Unit step (pure confirmation) nor a complex value is // inline-editable, so Edit is greyed and the menu opens on Skip. - let mut it = Interaction::begin(&[], Value::Unitus); + let mut it = Prompt::begin(&[], Value::Unitus); it.handle(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); assert_eq!( it.handle(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)), @@ -336,7 +335,7 @@ fn esc_menu_disables_edit_for_unit_and_complex() { ); let tablet = Value::Tabularum(vec![("k".to_string(), Value::Unitus)]); - let mut it = Interaction::begin(&[], tablet); + let mut it = Prompt::begin(&[], tablet); it.handle(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); assert_eq!( it.handle(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)), @@ -348,7 +347,7 @@ fn esc_menu_disables_edit_for_unit_and_complex() { fn menu_shows_greyed_edit_when_unavailable() { // Edit is always listed so it stays discoverable; for a non-editable value // it is drawn (greyed) alongside the exits, and the menu opens on Skip. - let mut it = Interaction::begin(&[], Value::Unitus); + let mut it = Prompt::begin(&[], Value::Unitus); it.handle(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); let mut out: Vec = Vec::new(); draw(&mut out, "I/1", "→", &it).expect("draw"); @@ -362,7 +361,7 @@ fn fail_reason_backs_out_through_menu_to_field() { // Fail opens the reason submenu; Esc closes it back to the menu (Fail still // selectable), and a second Esc returns to the untouched frozen value, so // Enter still completes the step with its produced value intact. - let mut it = Interaction::begin(&[], Value::Literali("eth0".to_string())); + let mut it = Prompt::begin(&[], Value::Literali("eth0".to_string())); it.handle(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); it.handle(KeyEvent::new(KeyCode::Right, KeyModifiers::NONE)); it.handle(KeyEvent::new(KeyCode::Right, KeyModifiers::NONE)); @@ -387,7 +386,7 @@ fn fail_reason_backs_out_through_menu_to_field() { #[test] fn fail_reason_reopens_empty_after_abandon() { // Abandoning a reason discards its text; reopening Fail starts fresh. - let mut it = Interaction::begin(&[], Value::Unitus); + let mut it = Prompt::begin(&[], Value::Unitus); it.handle(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); it.handle(KeyEvent::new(KeyCode::Right, KeyModifiers::NONE)); it.handle(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); @@ -404,7 +403,7 @@ fn fail_reason_reopens_empty_after_abandon() { #[test] fn esc_menu_backs_out_to_field() { - let mut it = Interaction::begin(&[], Value::Literali("x".to_string())); + let mut it = Prompt::begin(&[], Value::Literali("x".to_string())); it.handle(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); // Esc out of the menu returns to the frozen value; Enter accepts it intact. assert_eq!( @@ -419,9 +418,9 @@ fn esc_menu_backs_out_to_field() { #[test] fn choices_navigate_and_accept() { - let mut it = Interaction::begin(&["Yes", "No"], Value::Unitus); + let mut it = Prompt::begin(&["Yes", "No"], Value::Unitus); // First choice is the default. - let mut first = Interaction::begin(&["Yes", "No"], Value::Unitus); + let mut first = Prompt::begin(&["Yes", "No"], Value::Unitus); assert_eq!( first.handle(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)), Some(UserInput::Done(Value::Literali("Yes".to_string()))) @@ -439,7 +438,7 @@ fn choices_navigate_and_accept() { #[test] fn choices_esc_opens_menu() { - let mut it = Interaction::begin(&["Yes", "No"], Value::Unitus); + let mut it = Prompt::begin(&["Yes", "No"], Value::Unitus); it.handle(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); assert_eq!( it.handle(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)), @@ -454,7 +453,7 @@ fn read_only_values_accept_intact() { "name".to_string(), Value::Literali("eth0".to_string()), )]); - let mut it = Interaction::begin(&[], tablet.clone()); + let mut it = Prompt::begin(&[], tablet.clone()); assert_eq!( it.handle(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE)), None @@ -465,7 +464,7 @@ fn read_only_values_accept_intact() { ); let dump = Value::Literali("1: lo\n2: eth0\n3: wlan0".to_string()); - let mut it = Interaction::begin(&[], dump.clone()); + let mut it = Prompt::begin(&[], dump.clone()); assert_eq!( it.handle(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE)), None @@ -482,7 +481,7 @@ fn render_frozen_shows_only_triangle() { // prompt line, and the menu options are not advertised — the normal prompt // is just the "play" triangle. let dump = Value::Literali("1: lo\n2: eth0\n3: wlan0".to_string()); - let it = Interaction::begin(&[], dump); + let it = Prompt::begin(&[], dump); let mut out: Vec = Vec::new(); draw(&mut out, "I/1", "→", &it).expect("draw"); let written = String::from_utf8(out).expect("utf8"); @@ -493,7 +492,7 @@ fn render_frozen_shows_only_triangle() { #[test] fn ctrl_c_quits_from_any_field() { - let mut it = Interaction::begin(&[], Value::Unitus); + let mut it = Prompt::begin(&[], Value::Unitus); assert_eq!( it.handle(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL)), Some(UserInput::Quit) @@ -502,7 +501,7 @@ fn ctrl_c_quits_from_any_field() { #[test] fn render_edit_shows_candidate_text() { - let mut it = Interaction::begin(&[], Value::Literali("hello".to_string())); + let mut it = Prompt::begin(&[], Value::Literali("hello".to_string())); // Frozen by default; once edited, the candidate text is shown for editing. it.handle(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); it.handle(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); @@ -516,7 +515,7 @@ fn render_edit_shows_candidate_text() { fn render_reason_replaces_menu() { // Choosing Fail replaces the menu with the reason prompt on the same line, // keeping the ▶ prefix; the menu items are gone, and it stays one line. - let mut it = Interaction::begin(&[], Value::Unitus); + let mut it = Prompt::begin(&[], Value::Unitus); it.handle(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); it.handle(KeyEvent::new(KeyCode::Right, KeyModifiers::NONE)); it.handle(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); @@ -533,7 +532,7 @@ fn render_reason_replaces_menu() { #[test] fn render_choices_lists_options() { - let it = Interaction::begin(&["Yes", "No"], Value::Unitus); + let it = Prompt::begin(&["Yes", "No"], Value::Unitus); let mut out: Vec = Vec::new(); draw(&mut out, "I/1", "→", &it).expect("draw"); let written = String::from_utf8(out).expect("utf8"); @@ -552,8 +551,8 @@ fn is_list_forma_recognises_brackets() { } // A bracketed list field, as prompt_acquire seeds it for an iterated binding. -fn list_prompt() -> Interaction { - Interaction { +fn list_prompt() -> Prompt { + Prompt { field: edit(String::new(), Value::Literali(String::new()), true), menu: None, reason: None, diff --git a/src/runner/checks/runner.rs b/src/runner/checks/runner.rs index 33972a4c..d6e84f6c 100644 --- a/src/runner/checks/runner.rs +++ b/src/runner/checks/runner.rs @@ -12,7 +12,9 @@ use crate::resolution::resolve; use crate::runner::driver::{Automatic, Event, Mock, UserInput}; use crate::runner::evaluator::Environment; use crate::runner::library::Library; -use crate::runner::runner::{bind_parameters, render_argument_echo, Outcome, Runner, RunnerError}; +use crate::runner::runner::{ + bind_parameters, render_argument_echo, Conclusion, Outcome, Runner, RunnerError, +}; use crate::runner::state::{parse_record, Appender, InvokeTarget, State, Store, Supplied}; use crate::translation::translate; use crate::value::Value; @@ -140,7 +142,7 @@ fn step_outcomes_recorded() { let outcome = runner .run(env) .expect("run"); - assert_eq!(outcome, Outcome::Done(Value::Unitus)); + assert_eq!(outcome, Conclusion::Completed(Outcome::Done(Value::Unitus))); let pfftt = fixture.pfftt_contents(); let lines: Vec<&str> = pfftt .lines() @@ -418,7 +420,7 @@ fn quit_propagates_and_stops_walking() { let outcome = runner .run(env) .expect("run"); - assert_eq!(outcome, Outcome::Stopped); + assert_eq!(outcome, Conclusion::Stopping); let prompt = runner.into_driver(); let step_fqns: Vec<&str> = prompt @@ -745,7 +747,7 @@ cycle(s) : Situation -> Done let outcome = runner .run(Environment::new()) .expect("run"); - assert_eq!(outcome, Outcome::Stopped); + assert_eq!(outcome, Conclusion::Stopping); let pfftt = fixture.pfftt_contents(); assert!( @@ -1239,7 +1241,7 @@ check : .run(Environment::new()) .expect("run"); match outcome { - Outcome::Done(_) => {} + Conclusion::Completed(Outcome::Done(_)) => {} other => panic!("expected Done, got {:?}", other), } let trace = String::from_utf8( @@ -1287,7 +1289,7 @@ check : .run(Environment::new()) .expect("run"); match outcome { - Outcome::Failed(_) => {} + Conclusion::Completed(Outcome::Fail(_)) => {} other => panic!("expected Failed, got {:?}", other), } @@ -1341,7 +1343,7 @@ fn interactive_override_severs_the_rollup_to_done() { .run(Environment::new()) .expect("run"); match outcome { - Outcome::Done(_) => {} + Conclusion::Completed(Outcome::Done(_)) => {} other => panic!("expected Done after override, got {:?}", other), } } @@ -1371,7 +1373,7 @@ fn interactive_accepting_a_failure_propagates() { .run(Environment::new()) .expect("run"); match outcome { - Outcome::Failed(_) => {} + Conclusion::Completed(Outcome::Fail(_)) => {} other => panic!("expected Failed, got {:?}", other), } } @@ -2347,7 +2349,7 @@ test : #[test] fn automatic_records_done_for_computable_step_skip_for_prose() { - fn record_of(label: &str, body: Operation<'static>) -> (Outcome, State) { + fn record_of(label: &str, body: Operation<'static>) -> (Conclusion, State) { let mut fixture = StoreFixture::new(label); let program = anonymous_with_body(Operation::Sequence(vec![step( Ordinal::Dependent("1"), @@ -2385,7 +2387,7 @@ fn automatic_records_done_for_computable_step_skip_for_prose() { ); assert_eq!( outcome, - Outcome::Done(Value::Literali("probe output".to_string())) + Conclusion::Completed(Outcome::Done(Value::Literali("probe output".to_string()))) ); assert_eq!( state, @@ -2400,7 +2402,9 @@ fn automatic_records_done_for_computable_step_skip_for_prose() { ); assert_eq!( outcome, - Outcome::Done(Value::Literali("1: lo\n2: eth0\n3: wlan0".to_string())) + Conclusion::Completed(Outcome::Done(Value::Literali( + "1: lo\n2: eth0\n3: wlan0".to_string() + ))) ); assert_eq!( state, @@ -2441,7 +2445,7 @@ fn sequence_value_is_last_member() { .expect("run"); assert_eq!( outcome, - Outcome::Done(Value::Literali("second".to_string())) + Conclusion::Completed(Outcome::Done(Value::Literali("second".to_string()))) ); let pfftt = fixture.pfftt_contents(); @@ -2497,7 +2501,7 @@ fn deferred_invoke_is_prompted_and_recorded() { let outcome = runner .run(Environment::new()) .expect("run"); - assert_eq!(outcome, Outcome::Done(Value::Unitus)); + assert_eq!(outcome, Conclusion::Completed(Outcome::Done(Value::Unitus))); // The operator was prompted about the external node, by its FQP. let prompt = runner.into_driver(); @@ -2910,7 +2914,7 @@ cleanup : let outcome = runner .run(Environment::new()) .expect("resume must not raise UnboundVariable"); - assert!(if let Outcome::Done(_) = outcome { + assert!(if let Conclusion::Completed(Outcome::Done(_)) = outcome { true } else { false @@ -3006,7 +3010,7 @@ fn resume_restores_invoke_input_without_reprompting() { let outcome = runner .run(Environment::new()) .expect("resume runs without re-acquiring"); - assert!(if let Outcome::Done(_) = outcome { + assert!(if let Conclusion::Completed(Outcome::Done(_)) = outcome { true } else { false diff --git a/src/runner/driver.rs b/src/runner/driver.rs index a6f08779..8f170554 100644 --- a/src/runner/driver.rs +++ b/src/runner/driver.rs @@ -51,7 +51,7 @@ pub enum UserInput { Quit, } -/// The default verdict a node's rolled-up body leaves standing at its sign-off. +/// The default verdict a node's rolled-up body leaves standing at its close. /// Declaration order encodes verdict precedence: Fail beats Done beats Skip. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum Standing { @@ -81,7 +81,7 @@ pub trait Driver { /// Cross out of this document on the way out: the `⇐` boundary line carrying /// the document name, version, and run identifier (`/ NetworkProbe,1 000096`), - /// closed by the run's rolled-up verdict glyph just as a scope's `↙` sign-off + /// closed by the run's rolled-up verdict glyph just as a scope's `↙` close /// is. Quit renders no glyph. fn conclude(&mut self, label: &str, verdict: &UserInput); @@ -117,7 +117,7 @@ pub trait Driver { /// before departing; the unattended drivers proceed with `Done`. fn depart(&mut self, qualified: &str) -> UserInput; - /// Settle an external invocation this run cannot perform (a `` call + /// Record an external invocation this run cannot perform (a `` call /// into another document or system). `Console` prompts the operator to /// attest it — `Done` if it was performed or recorded elsewhere, otherwise /// `Skip` / `Fail` / `Quit`. The unattended drivers return `Skip`: nothing @@ -126,22 +126,22 @@ pub trait Driver { fn external(&mut self, qualified: &str) -> UserInput; /// Gate a shell `exec` on the user's command: show the `script` to be run - /// and settle on the user's verdict. `Done` means run it now; Skip / Fail - /// decline the run and settle the step; Quit stops. `Automatic` runs + /// and act on the user's verdict. `Done` means run it now; Skip / Fail + /// decline the run and record the step; Quit stops. `Automatic` runs /// unconditionally, returning `Done` without prompting. fn command(&mut self, qualified: &str, script: &str) -> UserInput; /// Present a physical `Action` (a `browser`-library call like /// `click("Actions")`) the user performs themselves: show the imperative - /// `verb` and its `label` read-only on the prompt line and settle on the + /// `verb` and its `label` read-only on the prompt line and act on the /// user's verdict. `Done` means they did it, leaving a compact `{name}()` - /// trace; Skip / Fail decline and settle the step; Quit stops. Unlike + /// trace; Skip / Fail decline and record the step; Quit stops. Unlike /// `command` there is no edit buffer. `Automatic` returns `Done` without /// prompting. fn action(&mut self, qualified: &str, name: &str, verb: &str, label: &str) -> UserInput; /// Open a Section: the grey `↘ /fqp` descent bracket (matching the `↙` - /// the section's sign-off closes with) followed by its prose heading — + /// the section's close marker) followed by its prose heading — /// numeral and title, e.g. `II. Check internet connectivity`. fn section(&mut self, qualified: &str, numeral: &str, title: &str); @@ -384,7 +384,7 @@ impl Output for Silent { } /// The interactive verdict policy: prompt the user on the output's surface -/// and read their keystrokes. The keystroke logic lives in `Interaction`. The +/// and read their keystrokes. The keystroke logic lives in `Prompt`. The /// default is confirmation: `` completes the step, accepting the /// default action or the body's current value intact; the `` menu offers /// Skip, Fail, Quit, sometimes Override, and (for an editable scalar) Edit. @@ -741,7 +741,7 @@ fn prompt( produced: Value, ) -> UserInput { let qualified = display_path(qualified); - let result = interact(out, Interaction::begin(choices, produced), |o, i| { + let result = interact(out, Prompt::begin(choices, produced), |o, i| { draw(o, &qualified, settle, i) }); let _ = queue!( @@ -760,7 +760,7 @@ fn prompt_overrule( standing: Standing, ) -> UserInput { let qualified = display_path(qualified); - let result = interact(out, Interaction::overrule(standing), |o, i| { + let result = interact(out, Prompt::overrule(standing), |o, i| { draw(o, &qualified, marker, i) }); let _ = queue!( @@ -784,7 +784,7 @@ fn prompt_action( label: &str, ) -> UserInput { let qualified = display_path(qualified); - let result = interact(out, Interaction::begin(&[], Value::Unitus), |o, i| { + let result = interact(out, Prompt::begin(&[], Value::Unitus), |o, i| { draw_action(o, &qualified, verb, label, i) }); let _ = queue!( @@ -815,7 +815,7 @@ fn prompt_command(mut out: &mut dyn Write, qualified: &str, script: &str) -> Use ); let result = interact( out, - Interaction { + Prompt { field, menu: None, reason: None, @@ -848,7 +848,7 @@ fn prompt_acquire(mut out: &mut dyn Write, label: &str, list: bool) -> UserInput let field = edit(String::new(), Value::Literali(String::new()), list); let result = interact( out, - Interaction { + Prompt { field, menu: None, reason: None, @@ -870,8 +870,8 @@ fn prompt_acquire(mut out: &mut dyn Write, label: &str, list: bool) -> UserInput /// caller writes whatever record line it wants afterward. fn interact( mut out: &mut dyn Write, - mut interaction: Interaction, - mut render: impl FnMut(&mut dyn Write, &Interaction) -> io::Result<()>, + mut interaction: Prompt, + mut render: impl FnMut(&mut dyn Write, &Prompt) -> io::Result<()>, ) -> UserInput { // The interactive path is guarded on stdout being a terminal before the // walk begins, so a raw-mode failure here is an unexpected terminal fault @@ -967,7 +967,7 @@ fn render_settle( } /// Render the run's closing `⇐` boundary line, the rolled-up verdict glyph -/// following the label just as a scope's `↙` sign-off carries its own. Quit +/// following the label just as a scope's `↙` close carries its own. Quit /// renders nothing, as in `render_settle`. fn render_conclude(out: &mut W, label: &str, verdict: &UserInput, renderer: &dyn Render) { let (glyph, syntax) = match verdict_glyph(verdict) { @@ -1124,7 +1124,7 @@ struct Reason { /// folds one key into the state, returning `Some(UserInput)` once the /// operator has settled on an outcome. `reason` is the Fail submenu, open only /// while `menu` rests on Fail. -struct Interaction { +struct Prompt { field: Field, menu: Option, reason: Option, @@ -1134,7 +1134,7 @@ struct Interaction { standing: Standing, } -impl Interaction { +impl Prompt { /// Seed an interaction with the supplied choices and a Value. The /// behaviour on pressing is confirmation that the step is done, /// not data entry: `` completes the step and the step's value is @@ -1160,7 +1160,7 @@ impl Interaction { active: 0, } }; - Interaction { + Prompt { field, menu: None, reason: None, @@ -1172,7 +1172,7 @@ impl Interaction { /// (`Fail` or `Skip`). No value to accept, so the field is a frozen Unit; /// the standing colours the `▶` and, on `Fail`, lights up `Override`. fn overrule(standing: Standing) -> Self { - Interaction { + Prompt { field: Field::Frozen { produced: Value::Unitus, }, @@ -1446,7 +1446,7 @@ fn draw( mut out: &mut dyn Write, qualified: &str, settle: &str, - interaction: &Interaction, + interaction: &Prompt, ) -> io::Result<()> { queue!( &mut out, @@ -1488,7 +1488,7 @@ fn draw_action( qualified: &str, verb: &str, label: &str, - interaction: &Interaction, + interaction: &Prompt, ) -> io::Result<()> { queue!( &mut out, @@ -1516,7 +1516,7 @@ fn draw_action( /// by the step/command prompt and the action prompt. fn draw_tail( out: &mut dyn Write, - interaction: &Interaction, + interaction: &Prompt, prefix: u16, ) -> io::Result<(Option, u16)> { let mut cursor_col: Option = None; @@ -1675,11 +1675,7 @@ fn render_choices(mut out: &mut dyn Write, choices: &[&str], active: usize) -> i /// Render the Esc-menu: the active item in reverse video, a disabled item (a /// greyed `Edit`) dimmed, the rest plain. The active item is always enabled, so /// reverse and dim never apply to the same item. -fn render_menu( - mut out: &mut dyn Write, - interaction: &Interaction, - active: usize, -) -> io::Result<()> { +fn render_menu(mut out: &mut dyn Write, interaction: &Prompt, active: usize) -> io::Result<()> { for (i, item) in MENU .iter() .enumerate() @@ -2177,10 +2173,10 @@ impl Driver for Mock { UserInput::Done(Value::Unitus) } - /// A scope sign-off auto-accepts (records `Done`) and records the event, + /// A scope close auto-accepts (records `Done`) and records the event, /// rather than draining the `ask` answer queue — the structural-scope /// close is orthogonal to the step verdicts a test drives. A test - /// asserting sign-off behaviour inspects the recorded `Seal` event. + /// asserting close behaviour inspects the recorded `Seal` event. fn seal(&mut self, qualified: &str, _produced: Value, _computable: bool) -> UserInput { self.events .push(Event::Seal { diff --git a/src/runner/mod.rs b/src/runner/mod.rs index eefb9448..cc476dc9 100644 --- a/src/runner/mod.rs +++ b/src/runner/mod.rs @@ -20,7 +20,7 @@ pub use context::Context; pub use driver::{Headless, Mode}; pub use evaluator::Environment; pub use library::{library_for, Builtin, Library, Native}; -pub use runner::{Outcome, Runner, RunnerError}; +pub use runner::{Conclusion, Outcome, Runner, RunnerError}; pub use state::{Appender, RecordError, RunId}; use driver::{Automatic, Console, Transcript}; @@ -43,7 +43,7 @@ pub fn start<'i>( arguments: &[String], library: Library, libraries: &[String], -) -> Result<(RunId, Outcome), RunnerError> { +) -> Result<(RunId, Conclusion), RunnerError> { let env = bind_parameters(program, arguments)?; let store = Store::new(PathBuf::from(STORE_ROOT)); let (run_id, run_dir) = store.create(document, now_iso8601(), libraries)?; @@ -92,7 +92,7 @@ pub fn inspect<'i>( program: &'i Program<'i>, arguments: &[String], library: Library, -) -> Result { +) -> Result { let env = bind_parameters(program, arguments)?; match mode { Mode::Interactive => { @@ -141,7 +141,7 @@ pub fn resume<'i>( run_id: RunId, program: &'i Program<'i>, library: Library, -) -> Result { +) -> Result { if !std::io::stdout().is_terminal() { return Err(RunnerError::TerminalRequired); } diff --git a/src/runner/runner.rs b/src/runner/runner.rs index 41aae7e8..d0126574 100644 --- a/src/runner/runner.rs +++ b/src/runner/runner.rs @@ -17,25 +17,27 @@ use crate::program::{ }; use crate::value::Value; -/// What executing an Operation (or evaluating a Step at any scale) produced. -/// `Done(Value)` is the natural success — for a leaf Step the operator's -/// recorded value, for a Sequence / Section / procedure body the unit value -/// once the whole subtree is finished. `Skipped` and `Failed` are operator -/// verdicts on individual Steps. `Stopped` is a control signal that -/// propagates immediately up the call stack to halt the walk; the deliberate -/// quit was already recorded as a `State::Stop` lifecycle event, so this -/// carries no payload — a `technique resume` picks up from the first step -/// with no recorded outcome. +/// A step's result. `Done(Value)` is the natural success — for a leaf Step the +/// user's recorded value, for a Sequence / Section / procedure body the unit +/// value once the whole subtree is finished. `Skip` and `Fail` are the user's +/// verdicts on individual Steps. #[derive(Debug, Clone, PartialEq)] pub enum Outcome { Done(Value), /// Carries the body's computed value for block semantics; recorded as no value. - Skipped(Value), - Failed(Failure), - /// A failure thrown mid-body (a failed exec); propagates up to the - /// enclosing step, which catches it and settles as Fail. - Throw(Failure), - Stopped, + Skip(Value), + Fail(Failure), +} + +/// Wraps an `Outcome`, plus the control signals that propagate up the walk. A +/// failure thrown mid-body (i.e. a failed exec) is `Throwing`; it propagates +/// up to the enclosing step, which catches it and records a Fail. `Stopping` +/// shows that we are halting the walk immediately. +#[derive(Debug, Clone, PartialEq)] +pub enum Conclusion { + Completed(Outcome), + Throwing(Failure), + Stopping, } /// Why a Step failed. @@ -175,7 +177,7 @@ impl<'i, D: Driver> Runner<'i, D> { /// selection here is `program.subroutines[0]` — the synthetic /// anonymous wrapper if the document is top-level Steps, otherwise /// the first declared procedure. - pub fn run(&mut self, mut env: Environment) -> Result { + pub fn run(&mut self, mut env: Environment) -> Result { if let Some(document) = &self.document { let label = format!( "/ {},1 #{}", @@ -260,7 +262,7 @@ impl<'i, D: Driver> Runner<'i, D> { } let result = self.walk(&mut env, &entry.body); // A named entry procedure is a structural scope: a completed run closes - // with a final sign-off prompt at its path. A Quit or error walk skips + // with a final closing prompt at its path. A Quit or error walk skips // it — the run did not finish. An anonymous entry (a bare series of // steps) has no procedure to accept, so it just ends. let result = if name.is_some() { @@ -269,8 +271,11 @@ impl<'i, D: Driver> Runner<'i, D> { .render(); let computable = is_scope_computable(&entry.body); let sealed = match result { - Ok(Outcome::Stopped) => Ok(Outcome::Stopped), - Ok(outcome) => self.seal_scope(&qualified, outcome, computable), + Ok(Conclusion::Stopping) => Ok(Conclusion::Stopping), + Ok(Conclusion::Completed(outcome)) => self.seal_scope(&qualified, outcome, computable), + Ok(Conclusion::Throwing(failure)) => { + self.seal_scope(&qualified, Outcome::Fail(failure), computable) + } Err(error) => Err(error), }; self.path @@ -281,8 +286,8 @@ impl<'i, D: Driver> Runner<'i, D> { }; // 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 { + if let Ok(conclusion) = &result { + if let Conclusion::Stopping = conclusion { } else { self.record_finish()?; if let Some(document) = &self.document { @@ -293,7 +298,7 @@ impl<'i, D: Driver> Runner<'i, D> { .run_id() .render() ); - let verdict = verdict_from(outcome); + let verdict = verdict_from(conclusion); self.driver .conclude(&label, &verdict); } @@ -306,7 +311,7 @@ impl<'i, D: Driver> Runner<'i, D> { &mut self, env: &mut Environment, op: &'i Operation<'i>, - ) -> Result { + ) -> Result { match op { Operation::Sequence(ops) => self.walk_sequence(env, ops), Operation::Prologue(ops) => { @@ -350,7 +355,7 @@ impl<'i, D: Driver> Runner<'i, D> { // their say-so. An `Action` (e.g. `click`) is a physical // interaction the user performs themselves: show the call // read-only to confirm. Either way Skip or Fail declines and - // settles the step; Quit stops. `Pure` builtins just announce + // records the step; Quit stops. `Pure` builtins just announce // and run. let nature = match &executable.target { ExecutableRef::Resolved(id) => self @@ -390,12 +395,12 @@ impl<'i, D: Driver> Runner<'i, D> { executable, Some(&[chosen]), ) { - Ok(value) => Ok(Outcome::Done(value)), + Ok(value) => Ok(Conclusion::Completed(Outcome::Done(value))), // A non-zero exit throws to fail the step // rather than aborting the run; the walk // continues. Err(RunnerError::CommandFailed(code)) => { - Ok(Outcome::Throw(Failure::Aborted(format!( + Ok(Conclusion::Throwing(Failure::Aborted(format!( "External command exited with status {}", code )))) @@ -403,8 +408,10 @@ impl<'i, D: Driver> Runner<'i, D> { Err(other) => Err(other), } } - UserInput::Skip => Ok(Outcome::Skipped(Value::Unitus)), - UserInput::Fail(reason) => Ok(Outcome::Throw(Failure::Aborted(reason))), + UserInput::Skip => Ok(Conclusion::Completed(Outcome::Skip(Value::Unitus))), + UserInput::Fail(reason) => { + Ok(Conclusion::Throwing(Failure::Aborted(reason))) + } UserInput::Override => { unreachable!() // a command prompt never offers Override } @@ -425,10 +432,12 @@ impl<'i, D: Driver> Runner<'i, D> { executable, None, )?; - Ok(Outcome::Done(value)) + Ok(Conclusion::Completed(Outcome::Done(value))) + } + UserInput::Skip => Ok(Conclusion::Completed(Outcome::Skip(Value::Unitus))), + UserInput::Fail(reason) => { + Ok(Conclusion::Throwing(Failure::Aborted(reason))) } - UserInput::Skip => Ok(Outcome::Skipped(Value::Unitus)), - UserInput::Fail(reason) => Ok(Outcome::Throw(Failure::Aborted(reason))), UserInput::Override => { unreachable!() // an action prompt never offers Override } @@ -445,18 +454,18 @@ impl<'i, D: Driver> Runner<'i, D> { executable, None, )?; - Ok(Outcome::Done(value)) + Ok(Conclusion::Completed(Outcome::Done(value))) } }?; // Pair the Execute with a Return carrying its value; a stopped // run leaves the enter unpaired. - let stopped = if let Outcome::Stopped = outcome { + let stopped = if let Conclusion::Stopping = outcome { true } else { false }; if effectful && !stopped { - let returned = if let Outcome::Done(value) = &outcome { + let returned = if let Conclusion::Completed(Outcome::Done(value)) = &outcome { Some(value.clone()) } else { None @@ -485,7 +494,7 @@ impl<'i, D: Driver> Runner<'i, D> { | Operation::Prose(_) | Operation::Hole => { let value = super::evaluator::evaluate(&self.library, &self.context, env, op)?; - Ok(Outcome::Done(value)) + Ok(Conclusion::Completed(Outcome::Done(value))) } } } @@ -586,7 +595,7 @@ impl<'i, D: Driver> Runner<'i, D> { &mut self, env: &mut Environment, invocable: &'i Invocable<'i>, - ) -> Result { + ) -> Result { match &invocable.target { SubroutineRef::Resolved(id) => { let subroutine = &self @@ -645,7 +654,7 @@ impl<'i, D: Driver> Runner<'i, D> { .completed .contains_key(&lexical) { - return Ok(Outcome::Done(Value::Unitus)); + return Ok(Conclusion::Completed(Outcome::Done(Value::Unitus))); } // Acquire deferred arguments at the call site, in the @@ -784,13 +793,18 @@ impl<'i, D: Driver> Runner<'i, D> { self.announce_procedure(subroutine, name, &lexical, &local); // Walk the callee's body in its own `local` environment, - // then sign off its scope; a Quit or error skips the - // sign-off, leaving the procedure unfinished. + // then close its scope; a Quit or error skips the close, + // leaving the procedure unfinished. let result = self.walk(&mut local, &subroutine.body); let computable = is_scope_computable(&subroutine.body); let sealed = match result { - Ok(Outcome::Stopped) => Ok(Outcome::Stopped), - Ok(outcome) => self.seal_scope(&lexical, outcome, computable), + Ok(Conclusion::Stopping) => Ok(Conclusion::Stopping), + Ok(Conclusion::Completed(outcome)) => { + self.seal_scope(&lexical, outcome, computable) + } + Ok(Conclusion::Throwing(failure)) => { + self.seal_scope(&lexical, Outcome::Fail(failure), computable) + } Err(error) => Err(error), }; self.path @@ -803,13 +817,13 @@ impl<'i, D: Driver> Runner<'i, D> { SubroutineRef::Unresolved(id) => { self.driver .announce(&format!("<{}>", id.value)); - Ok(Outcome::Done(Value::Unitus)) + Ok(Conclusion::Completed(Outcome::Done(Value::Unitus))) } SubroutineRef::Deferred(ext) => { // An external target lives in another document or system, so // this run cannot descend into it. Record the call site, then - // present the invocation as its own node for the operator to - // settle: Done if they performed (or recorded elsewhere) the + // present the invocation as its own node for the user to + // decide: Done if they performed (or recorded elsewhere) the // external procedure, otherwise Skip or Fail. An unattended // (automatic) run records Skip — nothing executed it and no one // is present to attest it, so it is not marked Done. @@ -841,7 +855,7 @@ impl<'i, D: Driver> Runner<'i, D> { { self.path .pop(); - return Ok(Outcome::Done(Value::Unitus)); + return Ok(Conclusion::Completed(Outcome::Done(Value::Unitus))); } self.begin_scope(&qualified)?; @@ -870,17 +884,17 @@ impl<'i, D: Driver> Runner<'i, D> { self.driver .show_verdict("⇐", &qualified, &input); - let outcome = outcome_from(input); + let conclusion = outcome_from(input); self.appender .append(&Record { recorded: now_iso8601(), run_id, path: qualified, - state: record_state(&outcome), + state: record_state(&conclusion), })?; self.path .pop(); - Ok(outcome) + Ok(conclusion) } } } @@ -903,7 +917,7 @@ impl<'i, D: Driver> Runner<'i, D> { names: &'i [language::Identifier<'i>], value: &'i Operation<'i>, inferred: Option<&'i language::Genus<'i>>, - ) -> Result { + ) -> Result { let descriptive = if let Operation::Sequence(ops) = value { ops.is_empty() } else { @@ -942,10 +956,10 @@ impl<'i, D: Driver> Runner<'i, D> { Value::Unitus, )?; } - return Ok(Outcome::Skipped(Value::Unitus)); + return Ok(Conclusion::Completed(Outcome::Skip(Value::Unitus))); } UserInput::Fail(reason) => { - return Ok(Outcome::Failed(Failure::Aborted(reason))) + return Ok(Conclusion::Completed(Outcome::Fail(Failure::Aborted(reason)))) } UserInput::Override => unreachable!(), // an acquire prompt never offers Override UserInput::Quit => return self.record_stop(), @@ -965,7 +979,7 @@ impl<'i, D: Driver> Runner<'i, D> { } else { Value::Parametriq(acquired) }; - Ok(Outcome::Done(produced)) + Ok(Conclusion::Completed(Outcome::Done(produced))) } else { // Walk rather than evaluate: the bound value may be an effectful // spine operation — an `Invoke` that must descend into its callee @@ -973,13 +987,13 @@ impl<'i, D: Driver> Runner<'i, D> { // the evaluator would mishandle as Unit. Walking a pure value is // equivalent to evaluating it. match self.walk(env, value)? { - Outcome::Done(value) => { + Conclusion::Completed(Outcome::Done(value)) => { super::evaluator::bind_names(env, names, value.clone())?; - Ok(Outcome::Done(value)) + Ok(Conclusion::Completed(Outcome::Done(value))) } - Outcome::Skipped(value) => { + Conclusion::Completed(Outcome::Skip(value)) => { super::evaluator::bind_names(env, names, Value::Unitus)?; - Ok(Outcome::Skipped(value)) + Ok(Conclusion::Completed(Outcome::Skip(value))) } other => Ok(other), } @@ -1001,13 +1015,13 @@ impl<'i, D: Driver> Runner<'i, D> { names: &'i [language::Identifier<'i>], over: Option<&'i Operation<'i>>, body: &'i Operation<'i>, - ) -> Result { + ) -> Result { match over { None => { let mut number = 1; loop { - if let Outcome::Stopped = self.walk_iteration(env, names, number, body)? { - return Ok(Outcome::Stopped); + if let Conclusion::Stopping = self.walk_iteration(env, names, number, body)? { + return Ok(Conclusion::Stopping); } number += 1; } @@ -1023,7 +1037,7 @@ impl<'i, D: Driver> Runner<'i, D> { .lookup(id.value) .is_none() { - return Ok(Outcome::Done(Value::Unitus)); + return Ok(Conclusion::Completed(Outcome::Done(Value::Unitus))); } } let value = super::evaluator::evaluate(&self.library, &self.context, env, expr)?; @@ -1037,16 +1051,16 @@ impl<'i, D: Driver> Runner<'i, D> { let number = i + 1; match self.walk_iteration(env, names, number, body)? { - Outcome::Stopped => return Ok(Outcome::Stopped), - Outcome::Throw(f) => return Ok(Outcome::Throw(f)), - other => rollup.absorb(other), + Conclusion::Stopping => return Ok(Conclusion::Stopping), + Conclusion::Throwing(f) => return Ok(Conclusion::Throwing(f)), + Conclusion::Completed(other) => rollup.absorb(other), } } // A loop yields unit, so discard the rolled-up value while keeping its verdict. match rollup.settle() { - Outcome::Done(_) => Ok(Outcome::Done(Value::Unitus)), - Outcome::Skipped(_) => Ok(Outcome::Skipped(Value::Unitus)), - other => Ok(other), + Outcome::Done(_) => Ok(Conclusion::Completed(Outcome::Done(Value::Unitus))), + Outcome::Skip(_) => Ok(Conclusion::Completed(Outcome::Skip(Value::Unitus))), + other => Ok(Conclusion::Completed(other)), } } } @@ -1062,7 +1076,7 @@ impl<'i, D: Driver> Runner<'i, D> { names: &'i [language::Identifier<'i>], number: usize, body: &'i Operation<'i>, - ) -> Result { + ) -> Result { self.path .push(PathSegment::Iteration(number)); let qualified = self @@ -1073,9 +1087,11 @@ impl<'i, D: Driver> Runner<'i, D> { .enter(&echo); let result = self.walk(env, body); let verdict = match &result { - Ok(Outcome::Done(_)) => Some(UserInput::Done(Value::Unitus)), - Ok(Outcome::Skipped(_)) => Some(UserInput::Skip), - Ok(Outcome::Failed(Failure::Aborted(reason))) => Some(UserInput::Fail(reason.clone())), + Ok(Conclusion::Completed(Outcome::Done(_))) => Some(UserInput::Done(Value::Unitus)), + Ok(Conclusion::Completed(Outcome::Skip(_))) => Some(UserInput::Skip), + Ok(Conclusion::Completed(Outcome::Fail(Failure::Aborted(reason)))) => { + Some(UserInput::Fail(reason.clone())) + } _ => None, }; if let Some(verdict) = verdict { @@ -1091,7 +1107,7 @@ impl<'i, D: Driver> Runner<'i, D> { &mut self, env: &mut Environment, ops: &'i [Operation<'i>], - ) -> Result { + ) -> Result { let mut parallel_idx: usize = 0; let mut rollup = Rollup::new(); for op in ops { @@ -1111,19 +1127,19 @@ impl<'i, D: Driver> Runner<'i, D> { // A prose child contributes its value but no verdict, so it cannot // make an otherwise-skipped sequence roll up as Done. if let Operation::Prose(_) = op { - if let Outcome::Done(value) = outcome { + if let Conclusion::Completed(Outcome::Done(value)) = outcome { rollup.observe(value); } continue; } match outcome { // Stopped and Throw abandon the sequence at once; a Fail rolls up. - Outcome::Stopped => return Ok(Outcome::Stopped), - Outcome::Throw(failure) => return Ok(Outcome::Throw(failure)), - other => rollup.absorb(other), + Conclusion::Stopping => return Ok(Conclusion::Stopping), + Conclusion::Throwing(failure) => return Ok(Conclusion::Throwing(failure)), + Conclusion::Completed(other) => rollup.absorb(other), } } - Ok(rollup.settle()) + Ok(Conclusion::Completed(rollup.settle())) } fn walk_section( @@ -1132,7 +1148,7 @@ impl<'i, D: Driver> Runner<'i, D> { numeral: &'i str, title: Option<&'i Operation<'i>>, body: &'i Operation<'i>, - ) -> Result { + ) -> Result { self.path .push(PathSegment::Section(numeral)); let qualified = self @@ -1144,7 +1160,7 @@ impl<'i, D: Driver> Runner<'i, D> { { self.path .pop(); - return Ok(Outcome::Done(Value::Unitus)); + return Ok(Conclusion::Completed(Outcome::Done(Value::Unitus))); } self.begin_scope(&qualified)?; let result = self.perform_section(env, numeral, title, body); @@ -1155,8 +1171,11 @@ impl<'i, D: Driver> Runner<'i, D> { // close before the next sibling runs. A Quit or error walk skips the // prompt — the section did not complete. match result { - Ok(Outcome::Stopped) => Ok(Outcome::Stopped), - Ok(outcome) => self.seal_scope(&qualified, outcome, computable), + Ok(Conclusion::Stopping) => Ok(Conclusion::Stopping), + Ok(Conclusion::Completed(outcome)) => self.seal_scope(&qualified, outcome, computable), + Ok(Conclusion::Throwing(failure)) => { + self.seal_scope(&qualified, Outcome::Fail(failure), computable) + } Err(error) => Err(error), } } @@ -1167,7 +1186,7 @@ impl<'i, D: Driver> Runner<'i, D> { numeral: &'i str, title: Option<&'i Operation<'i>>, body: &'i Operation<'i>, - ) -> Result { + ) -> Result { let qualified = self .path .render(); @@ -1188,7 +1207,7 @@ impl<'i, D: Driver> Runner<'i, D> { env: &mut Environment, op: &'i Operation<'i>, parallel_index: usize, - ) -> Result { + ) -> Result { let Operation::Step { ordinal, attributes, @@ -1228,14 +1247,14 @@ impl<'i, D: Driver> Runner<'i, D> { // Walk the anonymous step-0 scope. Its `/0` address is bracketed // Begin…Done like a step's, so a completed prologue short-circuits on // resume (and rehydrates any bindings it made) rather than re-running its - // commands; unlike a step it takes no sign-off of its own, folding its + // commands; unlike a step it takes no closing prompt of its own, folding its // outcome into the enclosing procedure's seal. fn perform_prologue( &mut self, env: &mut Environment, qualified: &str, ops: &'i [Operation<'i>], - ) -> Result { + ) -> Result { if let Some(value) = self .completed .get(qualified) @@ -1254,21 +1273,24 @@ impl<'i, D: Driver> Runner<'i, D> { { super::evaluator::bind_names(env, names, value.clone())?; } - return Ok(Outcome::Done(value)); + return Ok(Conclusion::Completed(Outcome::Done(value))); } self.begin_scope(qualified)?; - let outcome = self.walk_sequence(env, ops)?; - if let Outcome::Stopped = outcome { - return Ok(outcome); + let conclusion = self.walk_sequence(env, ops)?; + if let Conclusion::Stopping = conclusion { + return Ok(conclusion); } - // A prologue takes a step's verdict rule: ending in prose, it settles - // Skip even when a command within it ran. + // A prologue takes a step's verdict rule: ending in prose, it records + // Skip even when a command within it ran. A thrown failure records Fail + // and still propagates. let computable = ops .last() .is_some_and(is_step_computable); - let outcome = match outcome { - Outcome::Done(value) if !computable => Outcome::Skipped(value), + let conclusion = match conclusion { + Conclusion::Completed(Outcome::Done(value)) if !computable => { + Conclusion::Completed(Outcome::Skip(value)) + } other => other, }; let record = Record { @@ -1277,11 +1299,11 @@ impl<'i, D: Driver> Runner<'i, D> { .appender .run_id(), path: qualified.to_string(), - state: record_state(&outcome), + state: record_state(&conclusion), }; self.appender .append(&record)?; - Ok(outcome) + Ok(conclusion) } fn perform_step( @@ -1289,7 +1311,7 @@ impl<'i, D: Driver> Runner<'i, D> { env: &mut Environment, qualified: &str, op: &'i Operation<'i>, - ) -> Result { + ) -> Result { let Operation::Step { source, body, @@ -1316,7 +1338,7 @@ impl<'i, D: Driver> Runner<'i, D> { } else if let Some(names) = binding_names(body) { super::evaluator::bind_names(env, names, value.clone())?; } - return Ok(Outcome::Done(value)); + return Ok(Conclusion::Completed(Outcome::Done(value))); } // Mark the start of work on this step before walking its body, @@ -1357,12 +1379,12 @@ impl<'i, D: Driver> Runner<'i, D> { Value::Unitus } else { match self.walk(env, body)? { - Outcome::Stopped => return Ok(Outcome::Stopped), - Outcome::Done(value) => value, + Conclusion::Stopping => return Ok(Conclusion::Stopping), + Conclusion::Completed(Outcome::Done(value)) => value, // A rolled-up child failure signs off through `overrule`: the // failure stands and propagates by default, but an interactive // run may Override it to Done, severing the rollup. - Outcome::Failed(_) => { + Conclusion::Completed(Outcome::Fail(_)) => { let input = self .driver .overrule(qualified, "→", Standing::Fail); @@ -1371,36 +1393,38 @@ impl<'i, D: Driver> Runner<'i, D> { } self.driver .show_verdict("→", qualified, &input); - let outcome = outcome_from(input); + let conclusion = outcome_from(input); let record = Record { recorded: now_iso8601(), run_id, path: qualified.to_string(), - state: record_state(&outcome), + state: record_state(&conclusion), }; self.appender .append(&record)?; - return Ok(outcome); + return Ok(conclusion); } - // The body settled itself — a declined command beat (Skip) or a + // The body recorded itself — a declined command beat (Skip) or a // thrown exec failure (caught here as a Fail). Record and show // its verdict without an acceptance prompt. settled => { - let settled = match settled { - Outcome::Throw(failure) => Outcome::Failed(failure), - other => other, + let outcome = match settled { + Conclusion::Throwing(failure) => Outcome::Fail(failure), + Conclusion::Completed(other) => other, + Conclusion::Stopping => unreachable!(), // Stopped returned above }; + let conclusion = Conclusion::Completed(outcome); let record = Record { recorded: now_iso8601(), run_id, path: qualified.to_string(), - state: record_state(&settled), + state: record_state(&conclusion), }; self.appender .append(&record)?; - let verdict = match &settled { - Outcome::Skipped(_) => UserInput::Skip, - Outcome::Failed(Failure::Aborted(reason)) => { + let verdict = match &conclusion { + Conclusion::Completed(Outcome::Skip(_)) => UserInput::Skip, + Conclusion::Completed(Outcome::Fail(Failure::Aborted(reason))) => { UserInput::Fail(reason.clone()) } // only Skip and Fail reach the settled branch @@ -1408,27 +1432,27 @@ impl<'i, D: Driver> Runner<'i, D> { }; self.driver .show_verdict("→", qualified, &verdict); - return Ok(settled); + return Ok(conclusion); } } }; // A descriptive binding already took the user's input at its acquire // prompt; that value (or a bare ) is the step's verdict, so - // settle Done without a redundant acceptance prompt. + // record Done without a redundant acceptance prompt. if responses.is_empty() && binds_descriptively(body) { - let outcome = Outcome::Done(produced); + let conclusion = Conclusion::Completed(Outcome::Done(produced)); self.driver - .show_verdict("→", qualified, &verdict_from(&outcome)); + .show_verdict("→", qualified, &verdict_from(&conclusion)); let record = Record { recorded: now_iso8601(), run_id, path: qualified.to_string(), - state: record_state(&outcome), + state: record_state(&conclusion), }; self.appender .append(&record)?; - return Ok(outcome); + return Ok(conclusion); } let choices: Vec<&str> = responses @@ -1450,20 +1474,24 @@ impl<'i, D: Driver> Runner<'i, D> { self.driver .show_verdict("→", qualified, &input); - let outcome = match input { - UserInput::Skip => Outcome::Skipped(propagate), + let conclusion = match input { + UserInput::Skip => Conclusion::Completed(Outcome::Skip(propagate)), other => outcome_from(other), }; // Bind the chosen response to the step's name(s); a skip binds Unitus // so a later reference resolves, mirroring a descriptive acquire. if binding_via_response { if let Some(names) = binding_names(body) { - match &outcome { - Outcome::Done(value) => { - super::evaluator::bind_names(env, names, value.clone())? + if let Conclusion::Completed(outcome) = &conclusion { + match outcome { + Outcome::Done(value) => { + super::evaluator::bind_names(env, names, value.clone())? + } + Outcome::Skip(_) => { + super::evaluator::bind_names(env, names, Value::Unitus)? + } + _ => {} } - Outcome::Skipped(_) => super::evaluator::bind_names(env, names, Value::Unitus)?, - _ => {} } } } @@ -1471,11 +1499,11 @@ impl<'i, D: Driver> Runner<'i, D> { recorded: now_iso8601(), run_id, path: qualified.to_string(), - state: record_state(&outcome), + state: record_state(&conclusion), }; self.appender .append(&record)?; - Ok(outcome) + Ok(conclusion) } /// Show a named procedure's heading on descent: the driver's `↘` enter line @@ -1616,10 +1644,10 @@ impl<'i, D: Driver> Runner<'i, D> { qualified: &str, outcome: Outcome, computable: bool, - ) -> Result { + ) -> Result { let standing = match &outcome { - Outcome::Failed(_) | Outcome::Throw(_) => Some(Standing::Fail), - Outcome::Skipped(_) => Some(Standing::Skip), + Outcome::Fail(_) => Some(Standing::Fail), + Outcome::Skip(_) => Some(Standing::Skip), _ => None, }; if let Some(standing) = standing { @@ -1645,7 +1673,7 @@ impl<'i, D: Driver> Runner<'i, D> { return Ok(settled); } let produced = match outcome { - Outcome::Done(value) | Outcome::Skipped(value) => value, + Outcome::Done(value) | Outcome::Skip(value) => value, _ => Value::Unitus, }; let propagate = produced.clone(); @@ -1660,28 +1688,28 @@ impl<'i, D: Driver> Runner<'i, D> { } self.driver .show_verdict("↙", qualified, &input); - let outcome = match input { - UserInput::Skip => Outcome::Skipped(propagate), + let conclusion = match input { + UserInput::Skip => Conclusion::Completed(Outcome::Skip(propagate)), other => outcome_from(other), }; let record = Record { recorded: now_iso8601(), run_id, path: qualified.to_string(), - state: record_state(&outcome), + state: record_state(&conclusion), }; self.appender .append(&record)?; - Ok(outcome) + Ok(conclusion) } - /// Settle an invocation declined at its acquire prompt: Skip and Fail + /// Record an invocation declined at its acquire prompt: Skip and Fail /// record the call's outcome at `qualified`; Quit stops the run. - fn abandon(&mut self, qualified: &str, input: UserInput) -> Result { + fn abandon(&mut self, qualified: &str, input: UserInput) -> Result { if let UserInput::Quit = input { return self.record_stop(); } - let outcome = outcome_from(input); + let conclusion = outcome_from(input); let run_id = self .appender .run_id(); @@ -1690,9 +1718,9 @@ impl<'i, D: Driver> Runner<'i, D> { recorded: now_iso8601(), run_id, path: qualified.to_string(), - state: record_state(&outcome), + state: record_state(&conclusion), })?; - Ok(outcome) + Ok(conclusion) } /// Record a `Finish` at the root path, closing a run that walked to its end. @@ -1711,7 +1739,7 @@ impl<'i, D: Driver> Runner<'i, D> { } /// Record a deliberate Stop at the root path and unwind the walk. - fn record_stop(&mut self) -> Result { + fn record_stop(&mut self) -> Result { let run_id = self .appender .run_id(); @@ -1723,7 +1751,7 @@ impl<'i, D: Driver> Runner<'i, D> { }; self.appender .append(&suspend)?; - Ok(Outcome::Stopped) + Ok(Conclusion::Stopping) } } @@ -1733,8 +1761,8 @@ fn describe_execute(function: &str) -> String { /// Accumulates child outcomes into one worst-wins verdict: the rank climbs by /// `Standing` precedence (Fail > Done > Skip) while the value tracks last-seen. -/// A `None` rank distinguishes an empty group (settles Done) from an all-skipped -/// one (settles Skip). +/// A `None` rank distinguishes an empty group (yields Done) from an all-skipped +/// one (yields Skip). struct Rollup { rank: Option, value: Value, @@ -1756,11 +1784,11 @@ impl Rollup { self.value = value; Standing::Done } - Outcome::Skipped(value) => { + Outcome::Skip(value) => { self.value = value; Standing::Skip } - Outcome::Failed(failure) => { + Outcome::Fail(failure) => { if self .failure .is_none() @@ -1769,8 +1797,6 @@ impl Rollup { } Standing::Fail } - // control-flow outcomes short-circuit before roll-up - Outcome::Throw(_) | Outcome::Stopped => unreachable!(), }; self.rank = Some(match self.rank { Some(cur) => cur.max(rank), @@ -1789,18 +1815,18 @@ impl Rollup { .rank .unwrap_or(Standing::Done) { - Standing::Fail => Outcome::Failed( + Standing::Fail => Outcome::Fail( self.failure .unwrap(), ), - Standing::Skip => Outcome::Skipped(self.value), + Standing::Skip => Outcome::Skip(self.value), Standing::Done => Outcome::Done(self.value), } } } /// Whether a scope holds any computable work, scanning every member. A scope of -/// pure prose is not computable, so an automatic run skips its sign-off. +/// pure prose is not computable, so an automatic run skips its close. fn is_scope_computable(op: &Operation) -> bool { match op { Operation::Sequence(ops) | Operation::Prologue(ops) => ops @@ -1838,40 +1864,41 @@ fn is_step_computable(op: &Operation) -> bool { } } -/// Lift a `UserInput` from the prompt into the runner's `Outcome`. An Override -/// settles `Done ()`, severing the rollup so the failed child below does not -/// propagate past this node. -fn outcome_from(input: UserInput) -> Outcome { +/// Lift a `UserInput` from the prompt into a `Conclusion`. An Override records +/// `Done ()`, severing the rollup so the failed child below does not propagate +/// past this node. A Quit becomes `Stopped`. +fn outcome_from(input: UserInput) -> Conclusion { match input { - UserInput::Done(value) => Outcome::Done(value), - UserInput::Override => Outcome::Done(Value::Unitus), - UserInput::Skip => Outcome::Skipped(Value::Unitus), - UserInput::Fail(reason) => Outcome::Failed(Failure::Aborted(reason)), - UserInput::Quit => Outcome::Stopped, + UserInput::Done(value) => Conclusion::Completed(Outcome::Done(value)), + UserInput::Override => Conclusion::Completed(Outcome::Done(Value::Unitus)), + UserInput::Skip => Conclusion::Completed(Outcome::Skip(Value::Unitus)), + UserInput::Fail(reason) => Conclusion::Completed(Outcome::Fail(Failure::Aborted(reason))), + UserInput::Quit => Conclusion::Stopping, } } -/// The closing line's verdict, rolling the run's final `Outcome` back into the -/// `UserInput` glyph the driver renders — mirroring the entry procedure's -/// sign-off. A `Done` shows `✓`, a `Skipped` `⊘`, a failure `✗`; the value and +/// The closing line's verdict, rolling the run's final `Conclusion` back into +/// the `UserInput` glyph the driver renders — mirroring the entry procedure's +/// close. A `Done` shows `✓`, a `Skip` `⊘`, a failure `✗`; the value and /// reason are immaterial to the glyph. -fn verdict_from(outcome: &Outcome) -> UserInput { - match outcome { - Outcome::Done(_) => UserInput::Done(Value::Unitus), - Outcome::Skipped(_) => UserInput::Skip, +fn verdict_from(conclusion: &Conclusion) -> UserInput { + match conclusion { + Conclusion::Completed(Outcome::Done(_)) => UserInput::Done(Value::Unitus), + Conclusion::Completed(Outcome::Skip(_)) => UserInput::Skip, _ => UserInput::Fail(String::new()), } } -/// Project the runner's in-memory `Outcome` into the on-disk `State` for the -/// PFFTT file. A `Done` records its full value (serialized by the state codec), -/// so a value bound with `~` rehydrates on resume. Quit is unreachable here: -/// the caller filters it out before recording. -fn record_state(outcome: &Outcome) -> State { - match outcome { - Outcome::Done(value) => State::Done(Some(value.clone())), - Outcome::Skipped(_) => State::Skip, - Outcome::Failed(Failure::Aborted(reason)) | Outcome::Throw(Failure::Aborted(reason)) => { +/// Project a `Conclusion` into the on-disk `State` for the PFFTT file. A `Done` +/// records its full value (serialized by the state codec), so a value bound +/// with `~` rehydrates on resume. A thrown failure records Fail. Stopped is +/// unreachable here: the caller filters it out before recording. +fn record_state(conclusion: &Conclusion) -> State { + match conclusion { + Conclusion::Completed(Outcome::Done(value)) => State::Done(Some(value.clone())), + Conclusion::Completed(Outcome::Skip(_)) => State::Skip, + Conclusion::Completed(Outcome::Fail(Failure::Aborted(reason))) + | Conclusion::Throwing(Failure::Aborted(reason)) => { if reason.is_empty() { // The user failed the step without giving a reason; record the // failure with no reason rather than an empty-string one. @@ -1880,7 +1907,7 @@ fn record_state(outcome: &Outcome) -> State { State::Fail(Some(super::state::fail_reason(reason))) } } - Outcome::Stopped => unreachable!(), // Stop is recorded as a lifecycle event, not a step result + Conclusion::Stopping => unreachable!(), // Stop is recorded as a lifecycle event, not a step result } } diff --git a/tests/runner/samples.rs b/tests/runner/samples.rs index f2605170..3dd10386 100644 --- a/tests/runner/samples.rs +++ b/tests/runner/samples.rs @@ -3,7 +3,9 @@ use std::fs; use std::path::Path; use technique::parsing; -use technique::runner::{Appender, Context, Environment, Headless, Library, Outcome, Runner}; +use technique::runner::{ + Appender, Conclusion, Context, Environment, Headless, Library, Outcome, Runner, +}; use technique::translation; use crate::common::list_technique_documents; @@ -113,10 +115,10 @@ fn ensure_run() { .skip(1) .collect(); - // A pure-prose procedure legitimately finishes Skipped under the - // automatic driver; only a Failed or Stopped run is a test failure. + // A pure-prose procedure legitimately finishes Skip under the + // automatic driver; only a Fail or Stopped run is a test failure. let finished = match outcome { - Outcome::Done(_) | Outcome::Skipped(_) => true, + Conclusion::Completed(Outcome::Done(_)) | Conclusion::Completed(Outcome::Skip(_)) => true, _ => { println!("File {:?} did not finish cleanly: {:?}", file, outcome); false From 3e255449f762fd1dae528f525f0237faf71ca964 Mon Sep 17 00:00:00 2001 From: Andrew Cowie Date: Wed, 1 Jul 2026 22:45:48 +1000 Subject: [PATCH 7/7] Describe steps with Kind to centralize logic --- src/main.rs | 2 +- src/runner/checks/driver.rs | 70 +++++++-- src/runner/checks/runner.rs | 33 ++-- src/runner/context.rs | 2 +- src/runner/driver.rs | 128 +++++++-------- src/runner/mod.rs | 2 +- src/runner/runner.rs | 175 +++++++++++---------- src/runner/state.rs | 4 +- src/value/types.rs | 7 +- tests/golden/runner/ExecutionInSteps.pfftt | 2 +- tests/runner/samples.rs | 4 +- 11 files changed, 237 insertions(+), 192 deletions(-) diff --git a/src/main.rs b/src/main.rs index 05d8bc47..6eb5df9e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -349,7 +349,7 @@ fn main() { .default_value("interactive") .action(ArgAction::Set) .conflicts_with_all(["interactive", "automatic", "quiet"]) - .help("How to walk the procedure: interactively, prompting the operator at each step; automatically, taking each step's computed value and running to completion or first failure; or quietly, also running automatically but suppressing all progress trace output, so that only the output of external commands is printed to the terminal."), + .help("How to walk the procedure: interactively, prompting the user at each step; automatically, taking each step's computed value and running to completion or first failure; or quietly, also running automatically but suppressing all progress trace output, so that only the output of external commands is printed to the terminal."), ) .arg( Arg::new("interactive") diff --git a/src/runner/checks/driver.rs b/src/runner/checks/driver.rs index 46be80a2..ceb1fbde 100644 --- a/src/runner/checks/driver.rs +++ b/src/runner/checks/driver.rs @@ -1,7 +1,8 @@ use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use crate::runner::driver::{ - draw, edit, is_list_forma, Automatic, Console, Driver, Event, Mock, Prompt, Standing, UserInput, + draw, edit, is_list_forma, Automatic, Console, Driver, Event, Kind, Mock, Prompt, Standing, + UserInput, }; use crate::value::{Numeric, Value}; @@ -13,18 +14,24 @@ fn mock_returns_canned_answers_in_order() { UserInput::Quit, ]); assert_eq!( - p.ask("/I/1", &[], Value::Unitus, true), + p.ask("/I/1", &[], Value::Unitus, Kind::Computable), UserInput::Done(Value::Unitus) ); - assert_eq!(p.ask("/I/1", &[], Value::Unitus, true), UserInput::Skip); - assert_eq!(p.ask("/I/1", &[], Value::Unitus, true), UserInput::Quit); + assert_eq!( + p.ask("/I/1", &[], Value::Unitus, Kind::Computable), + UserInput::Skip + ); + assert_eq!( + p.ask("/I/1", &[], Value::Unitus, Kind::Computable), + UserInput::Quit + ); } #[test] fn mock_records_step_and_ask_events() { let mut p = Mock::with_answers([UserInput::Done(Value::Unitus)]); p.step("/local_network:I/1", "Check the cable.", 1); - let _ = p.ask("/local_network:I/1", &[], Value::Unitus, true); + let _ = p.ask("/local_network:I/1", &[], Value::Unitus, Kind::Computable); assert_eq!( p.events(), &[ @@ -43,7 +50,7 @@ fn mock_records_step_and_ask_events() { #[test] fn mock_records_offered_choices() { let mut p = Mock::with_answers([UserInput::Done(Value::Literali("Yes".to_string()))]); - let _ = p.ask("I/1", &["Yes", "No"], Value::Unitus, true); + let _ = p.ask("I/1", &["Yes", "No"], Value::Unitus, Kind::Computable); assert_eq!( p.events(), &[Event::Ask { @@ -73,7 +80,7 @@ fn mock_records_enter_and_announce() { #[should_panic(expected = "Mock::ask called with no canned answers remaining")] fn mock_ask_without_answers_panics() { let mut p = Mock::new(); - let _ = p.ask("I/1", &[], Value::Unitus, true); + let _ = p.ask("I/1", &[], Value::Unitus, Kind::Computable); } #[test] @@ -103,15 +110,52 @@ fn console_step_indents_description_to_depth() { fn automatic_settles_done_when_computable_skip_otherwise() { let mut p = Automatic::with_handle(Vec::new()); assert_eq!( - p.ask("/I/1", &[], Value::Literali("ran".to_string()), true), + p.ask( + "/I/1", + &[], + Value::Literali("ran".to_string()), + Kind::Computable + ), UserInput::Done(Value::Literali("ran".to_string())) ); - assert_eq!(p.ask("/I/2", &[], Value::Unitus, false), UserInput::Skip); assert_eq!( - p.seal("/I", Value::Unitus, true), + p.ask("/I/2", &[], Value::Unitus, Kind::Prose), + UserInput::Skip + ); + assert_eq!( + p.seal("/I", Value::Unitus, Kind::Computable), UserInput::Done(Value::Unitus) ); - assert_eq!(p.seal("/II", Value::Unitus, false), UserInput::Skip); + assert_eq!(p.seal("/II", Value::Unitus, Kind::Prose), UserInput::Skip); +} + +#[test] +fn automatic_declines_action_and_choice_as_skip() { + // Unattended, a physical Action cannot be attested and a response Choice + // cannot be made, so both decline to Skip; a System command's output is + // taken as Done just like a plain Computable. + let mut p = Automatic::with_handle(Vec::new()); + assert_eq!( + p.action("/I/1", "click", "Click", "Actions"), + UserInput::Skip + ); + assert_eq!( + p.ask("/I/2", &["Yes", "No"], Value::Unitus, Kind::Choice), + UserInput::Skip + ); + assert_eq!( + p.ask("/I/3", &[], Value::Unitus, Kind::Action), + UserInput::Skip + ); + assert_eq!( + p.ask( + "/I/4", + &[], + Value::Literali("out".to_string()), + Kind::System + ), + UserInput::Done(Value::Literali("out".to_string())) + ); } #[test] @@ -208,7 +252,7 @@ fn override_inert_without_a_failure() { #[test] fn esc_edit_typed_enter_returns_literali() { // Editing is opt-in: Esc -> Edit (the first menu item) opens the buffer - // seeded from the value, which the operator can then extend. + // seeded from the value, which the user can then extend. let mut it = Prompt::begin(&[], Value::Literali("eth".to_string())); it.handle(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); assert_eq!( @@ -573,7 +617,7 @@ fn list_prompt_empty_submits_empty_list() { #[test] fn list_prompt_wraps_typed_buffer() { - // Whatever the operator types is wrapped in brackets on submit, so the + // Whatever the user types is wrapped in brackets on submit, so the // result parses through the existing list-literal path. let mut it = list_prompt(); for c in "east, west".chars() { diff --git a/src/runner/checks/runner.rs b/src/runner/checks/runner.rs index d6e84f6c..e0810616 100644 --- a/src/runner/checks/runner.rs +++ b/src/runner/checks/runner.rs @@ -437,7 +437,7 @@ fn quit_propagates_and_stops_walking() { // Only the first Step was prompted; the second never fired. assert_eq!(step_fqns, vec!["/1"]); - // Quit records the step's Begin (the operator started looking at it), then + // Quit records the step's Begin (the user started looking at it), then // a Stop lifecycle line at the root path — the deliberate-stop marker that // tells a quit from a crash. No Done/Skip/Fail for the step itself. let pfftt = fixture.pfftt_contents(); @@ -648,7 +648,7 @@ test : #[test] fn hole_argument_acquired_at_entry() { // main invokes cycle with `?`, declining to supply the Situation. The - // operator is asked for `s` once, when cycle is entered, and the bound + // user is asked for `s` once, when cycle is entered, and the bound // value serves both reads in the body. let source = r#" % technique v1 @@ -1421,14 +1421,15 @@ Prepare the ground { exec("true") } before the steps. .filter(|r| r.path == "/check:/0") .map(|r| &r.state) .collect(); - // The exec runs (its trace between Begin and the verdict), but the prologue - // ends in prose so it settles Skip. + // The exec runs (its trace between Begin and the outcome); the prologue + // holds real work, so it records that work's outcome — Done — rather than + // being stamped Skip by its prose tail. let State::Begin = zero[0] else { panic!("expected Begin first at /check:/0, got {:?}", zero[0]); }; - let State::Skip = zero[zero.len() - 1] else { + let State::Done(_) = zero[zero.len() - 1] else { panic!( - "expected Skip last at /check:/0, got {:?}", + "expected Done last at /check:/0, got {:?}", zero[zero.len() - 1] ); }; @@ -1503,7 +1504,7 @@ fn repeat_loops_until_quit() { let mut fixture = StoreFixture::new("repeat-until-quit"); // A `repeat` whose body is a single step. Each pass walks the step with - // a distinct `[n]` iteration segment; the operator quits on the third + // a distinct `[n]` iteration segment; the user quits on the third // pass, ending the loop. let inner = Operation::Step { ordinal: Ordinal::Dependent("1"), @@ -2466,8 +2467,8 @@ fn sequence_value_is_last_member() { #[test] fn deferred_invoke_is_prompted_and_recorded() { // A call to an external procedure this run cannot resolve (it lives in - // another document or system) is presented for the operator to settle: the - // run does not descend into it, but the operator can mark it Done (it was + // another document or system) is presented for the user to settle: the + // run does not descend into it, but the user can mark it Done (it was // performed, or recorded elsewhere), Skip, or Fail. The call site and the // settled outcome are both recorded. fn deferred_program() -> Program<'static> { @@ -2483,7 +2484,7 @@ fn deferred_invoke_is_prompted_and_recorded() { anonymous_with_body(Operation::Sequence(vec![invoke])) } - // The operator confirms the departure, then marks the external procedure + // The user confirms the departure, then marks the external procedure // Done at the return. let mut fixture = StoreFixture::new("deferred-done"); let program = deferred_program(); @@ -2503,7 +2504,7 @@ fn deferred_invoke_is_prompted_and_recorded() { .expect("run"); assert_eq!(outcome, Conclusion::Completed(Outcome::Done(Value::Unitus))); - // The operator was prompted about the external node, by its FQP. + // The user was prompted about the external node, by its FQP. let prompt = runner.into_driver(); let asked: Vec<&str> = prompt .events() @@ -2532,7 +2533,7 @@ fn deferred_invoke_is_prompted_and_recorded() { State::Done(Some(Value::Unitus)) ))); - // The operator declines: Skip is recorded at the external's FQP. (The + // The user declines: Skip is recorded at the external's FQP. (The // enclosing sequence proceeds and returns its last Done value, so the // run's overall outcome is not itself the Skip — that is walk_sequence's // concern, tested elsewhere; what matters here is the recorded Skip.) @@ -2558,7 +2559,7 @@ fn deferred_invoke_is_prompted_and_recorded() { .collect(); assert_eq!(settled, vec![State::Begin, State::Skip]); - // Under an automatic run there is no operator to attest the external work + // Under an automatic run there is no user to attest the external work // and nothing executed it, so it records Skip rather than a fabricated Done. let mut fixture = StoreFixture::new("deferred-automatic"); let program = deferred_program(); @@ -2584,7 +2585,7 @@ fn deferred_invoke_is_prompted_and_recorded() { #[test] fn descriptive_binding_acquires_list_for_foreach() { - // A descriptive `~ items` binding has no executable value, so the operator + // A descriptive `~ items` binding has no executable value, so the user // is asked to supply it. They enter a `[ … ]` literal, which coerces to a // list, and the following foreach walks its body once per element. let source = r#" @@ -2922,7 +2923,7 @@ cleanup : } // An elided invocation `` acquires its parameter `name` from the -// operator. The argument it was called with is recorded as an `Input` at the +// user. The argument it was called with is recorded as an `Input` at the // callee's path so a resume can restore it. const ACQUIRE_INPUT_SOURCE: &str = r#" % technique v1 @@ -2943,7 +2944,7 @@ fn invoke_records_supplied_input() { let mut program = translate(&document).expect("translate"); resolve(&mut program).expect("resolve"); - // The operator supplies "World" at the acquire prompt; the rest are step + // The user supplies "World" at the acquire prompt; the rest are step // and scope sign-offs. let prompt = Mock::with_answers([ UserInput::Done(Value::Literali("World".to_string())), diff --git a/src/runner/context.rs b/src/runner/context.rs index 6c7e442f..9409130f 100644 --- a/src/runner/context.rs +++ b/src/runner/context.rs @@ -1,5 +1,5 @@ //! Host capabilities available to native functions when they execute. For now -//! the only capability is passing output through to the operator: output goes +//! the only capability is passing output through to the user: output goes //! straight to standard output, or — for tests — into an in-memory buffer. A //! future GUI or web frontend would hold its own sink here, with //! `native()` staying the terminal default and a separate constructor carrying diff --git a/src/runner/driver.rs b/src/runner/driver.rs index 8f170554..164b6d36 100644 --- a/src/runner/driver.rs +++ b/src/runner/driver.rs @@ -60,15 +60,26 @@ pub enum Standing { Fail, } +/// The kind of step from the point of view of the runner.. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Kind { + Prose, // descriptive text; unit value + Computable, // a value will be computed with no external side effect + // (invoke, bind, literal, Pure builtin) + System, // an external command the host runs, e.g exec() + Action, // an act a human performs e.g. click()) + Choice, // a response the user must select +} + /// What the walker uses to drive a run. Implementations are the interactive -/// console `Console`, the no-operator `Automatic`, the no-output `Headless`, -/// the debugging `Transcript`, and the test `Mock`. +/// console `Console`, the unattended `Automatic`, the no-output `Headless`, +/// the debugging `Transcript`, and `Mock` for testing. pub trait Driver { - /// Show the step's Qualified Name and rendered description. `depth` is the - /// step's document nesting level (one-origin), indenting the description to - /// match while the `→` marker stays at the left margin. The implementation - /// displays them; it does not block waiting for input — the walker calls - /// `ask` for that separately. + /// Show the step's Qualified Name and rendered description. `depth` is + /// the step's document nesting level used to indent rendered source while + /// markers like `→` stay at the left margin. The implementation displays + /// them; it does not block waiting for input — the walker calls `ask` for + /// that separately. fn step(&mut self, qualified: &str, description: &str, depth: usize); /// Announce descent into a named scope — a Section or an invoked @@ -92,22 +103,15 @@ pub trait Driver { /// Execute / Unresolved Invoke announce-only, resume diagnostics. fn announce(&mut self, message: &str); - /// Answer the most recent `step` prompt. `produced` is the value the - /// step body computed, offered as the step's value: `Console` presents it - /// for the operator to accept, `Automatic` takes it directly. When - /// `choices` is non-empty the operator instead selects one of those - /// response values, yielding `Done(Literali(choice))`. Skip, fail, and - /// quit are available either way. `produced` is consumed: the driver - /// either moves it into the returned `Done` or discards it. `qualified` is - /// the step's Qualified Name, repeated on the live prompt line. `computable` - /// is whether the step has a body; an unattended driver `Done`s it, else `Skip`. - fn ask( - &mut self, - qualified: &str, - choices: &[&str], - produced: Value, - computable: bool, - ) -> UserInput; + /// Answer the most recent `step` prompt. `produced` is the value the step + /// body computed, offered as the step's value: `Console` presents it for + /// the user to accept, `Automatic` answers it directly. When `choices` is + /// non-empty the user instead selects one of those response values, + /// yielding `Done(Literali(choice))`. Skip, Fail, and Quit are available + /// to user either way. `produced` is consumed: the driver either moves it + /// into the returned `Done` or discards it. `qualified` is the step's + /// Qualified Name, repeated on the live prompt line. + fn ask(&mut self, qualified: &str, choices: &[&str], produced: Value, kind: Kind) -> UserInput; /// Cross out into an external `` procedure: prompt at the `⇒` departure /// — the place arguments would be solicited — then leave the glyph-less `⇒` @@ -118,7 +122,7 @@ pub trait Driver { fn depart(&mut self, qualified: &str) -> UserInput; /// Record an external invocation this run cannot perform (a `` call - /// into another document or system). `Console` prompts the operator to + /// into another document or system). `Console` prompts the user to /// attest it — `Done` if it was performed or recorded elsewhere, otherwise /// `Skip` / `Fail` / `Quit`. The unattended drivers return `Skip`: nothing /// executed it and no one is present to vouch that it was done, so the run @@ -145,12 +149,12 @@ pub trait Driver { /// numeral and title, e.g. `II. Check internet connectivity`. fn section(&mut self, qualified: &str, numeral: &str, title: &str); - /// Prompt the operator to sign off a completed structural scope — a Section - /// at its close, or the whole run at the entry procedure's close. Like - /// `ask` but with no response choices, settling to the `↙` close marker; - /// `produced` is the scope's value, offered for acceptance; `computable` - /// drives unattended `Done`/`Skip` as in `ask`. - fn seal(&mut self, qualified: &str, produced: Value, computable: bool) -> UserInput; + /// Prompt the user to sign off a completed structural scope — a + /// Section at its close, or the whole run at the entry procedure's close. + /// Like `ask` but with no response choices, settling to the `↙` close + /// marker; `produced` is the scope's value, offered for acceptance; + /// `kind` drives unattended `Done` or `Skip` as in `ask`. + fn seal(&mut self, qualified: &str, produced: Value, kind: Kind) -> UserInput; /// Sign off a node that rolled up to a failure or skip. When running /// interactively a failure may be Overridden to Done. @@ -219,7 +223,7 @@ pub trait Verdict { qualified: &str, choices: &[&str], produced: Value, - computable: bool, + kind: Kind, ) -> UserInput; fn seal( @@ -227,7 +231,7 @@ pub trait Verdict { out: &mut O, qualified: &str, produced: Value, - computable: bool, + kind: Kind, ) -> UserInput; fn command(&mut self, out: &mut O, qualified: &str, script: &str) -> UserInput; @@ -397,7 +401,7 @@ impl Verdict for Interactive { qualified: &str, choices: &[&str], produced: Value, - _computable: bool, + _kind: Kind, ) -> UserInput { prompt(out.surface(), qualified, "→", choices, produced) } @@ -407,7 +411,7 @@ impl Verdict for Interactive { out: &mut O, qualified: &str, produced: Value, - _computable: bool, + _kind: Kind, ) -> UserInput { prompt(out.surface(), qualified, "↙", &[], produced) } @@ -471,11 +475,10 @@ impl Verdict for Interactive { /// and scope's body value as the result, if available, otherwise Skip. pub struct Batch; -fn unattended(produced: Value, computable: bool) -> UserInput { - if computable { - UserInput::Done(produced) - } else { - UserInput::Skip +fn unattended(produced: Value, kind: Kind) -> UserInput { + match kind { + Kind::Computable | Kind::System => UserInput::Done(produced), + Kind::Prose | Kind::Action | Kind::Choice => UserInput::Skip, } } @@ -486,9 +489,9 @@ impl Verdict for Batch { _qualified: &str, _choices: &[&str], produced: Value, - computable: bool, + kind: Kind, ) -> UserInput { - unattended(produced, computable) + unattended(produced, kind) } fn seal( @@ -496,9 +499,9 @@ impl Verdict for Batch { _out: &mut O, _qualified: &str, produced: Value, - computable: bool, + kind: Kind, ) -> UserInput { - unattended(produced, computable) + unattended(produced, kind) } fn command(&mut self, out: &mut O, qualified: &str, script: &str) -> UserInput { @@ -515,7 +518,8 @@ impl Verdict for Batch { label: &str, ) -> UserInput { out.show_action(verb, label); - UserInput::Done(Value::Unitus) + // An unattended run cannot attest a physical action was performed. + UserInput::Skip } fn acquire( @@ -596,15 +600,9 @@ impl Driver for Interface { .section(qualified, numeral, title); } - fn ask( - &mut self, - qualified: &str, - choices: &[&str], - produced: Value, - computable: bool, - ) -> UserInput { + fn ask(&mut self, qualified: &str, choices: &[&str], produced: Value, kind: Kind) -> UserInput { self.verdict - .ask(&mut self.out, qualified, choices, produced, computable) + .ask(&mut self.out, qualified, choices, produced, kind) } fn depart(&mut self, qualified: &str) -> UserInput { @@ -627,9 +625,9 @@ impl Driver for Interface { .action(&mut self.out, qualified, name, verb, label) } - fn seal(&mut self, qualified: &str, produced: Value, computable: bool) -> UserInput { + fn seal(&mut self, qualified: &str, produced: Value, kind: Kind) -> UserInput { self.verdict - .seal(&mut self.out, qualified, produced, computable) + .seal(&mut self.out, qualified, produced, kind) } fn overrule(&mut self, qualified: &str, marker: &str, standing: Standing) -> UserInput { @@ -981,7 +979,7 @@ fn render_conclude(out: &mut W, label: &str, verdict: &UserInput, rend /// Render an automatically-run shell command on one line: `{path} $ {script}`, /// the whole line dark grey like the trace chrome so it reads as announce /// rather than as the command's own output that follows. The `$` stands in for -/// a shell prompt (distinct from the operator's `▶` edit prompt). Trailing +/// a shell prompt (distinct from the user's `▶` edit prompt). Trailing /// whitespace is trimmed so a code block's blank tail line is not echoed. fn render_command(out: &mut W, qualified: &str, script: &str, renderer: &dyn Render) { let line = renderer.style( @@ -1122,7 +1120,7 @@ struct Reason { /// Our state machine behind the "raw-mode" Console. `handle` /// folds one key into the state, returning `Some(UserInput)` once the -/// operator has settled on an outcome. `reason` is the Fail submenu, open only +/// user has chosen an outcome. `reason` is the Fail submenu, open only /// while `menu` rests on Fail. struct Prompt { field: Field, @@ -1897,16 +1895,10 @@ impl Driver for Transcript { .announce(message); } - fn ask( - &mut self, - qualified: &str, - choices: &[&str], - produced: Value, - computable: bool, - ) -> UserInput { + fn ask(&mut self, qualified: &str, choices: &[&str], produced: Value, kind: Kind) -> UserInput { let outcome = self .inner - .ask(qualified, choices, produced.clone(), computable); + .ask(qualified, choices, produced.clone(), kind); self.trace_outcome(qualified, produced, &outcome); outcome } @@ -1952,10 +1944,10 @@ impl Driver for Transcript { .section(qualified, numeral, title); } - fn seal(&mut self, qualified: &str, produced: Value, computable: bool) -> UserInput { + fn seal(&mut self, qualified: &str, produced: Value, kind: Kind) -> UserInput { let outcome = self .inner - .seal(qualified, produced.clone(), computable); + .seal(qualified, produced.clone(), kind); self.trace_outcome(qualified, produced, &outcome); outcome } @@ -2115,7 +2107,7 @@ impl Driver for Mock { qualified: &str, choices: &[&str], _produced: Value, - _computable: bool, + _kind: Kind, ) -> UserInput { self.events .push(Event::Ask { @@ -2177,7 +2169,7 @@ impl Driver for Mock { /// rather than draining the `ask` answer queue — the structural-scope /// close is orthogonal to the step verdicts a test drives. A test /// asserting close behaviour inspects the recorded `Seal` event. - fn seal(&mut self, qualified: &str, _produced: Value, _computable: bool) -> UserInput { + fn seal(&mut self, qualified: &str, _produced: Value, _kind: Kind) -> UserInput { self.events .push(Event::Seal { qualified: qualified.to_string(), diff --git a/src/runner/mod.rs b/src/runner/mod.rs index cc476dc9..bab4edda 100644 --- a/src/runner/mod.rs +++ b/src/runner/mod.rs @@ -1,5 +1,5 @@ //! Interactive runner that walks a translated Program step-by-step, -//! prompting the operator and recording each completed step to a state store +//! prompting the user and recording each completed step to a state store //! so a run can be resumed after interruption. use std::collections::HashMap; diff --git a/src/runner/runner.rs b/src/runner/runner.rs index d0126574..7124a151 100644 --- a/src/runner/runner.rs +++ b/src/runner/runner.rs @@ -5,7 +5,7 @@ use std::io; use std::path::PathBuf; use super::context::Context; -use super::driver::{Driver, Standing, UserInput}; +use super::driver::{Driver, Kind, Standing, UserInput}; use super::evaluator::Environment; use super::library::{Library, Nature}; use super::path::{PathSegment, QualifiedPath}; @@ -105,7 +105,7 @@ pub enum RunnerError { /// the position in the document via a `QualifiedPath` stack, carries an /// `Environment` with known result values. Holds the set of step FQNs already /// completed in a *prior* run — the resume snapshot plus an append handle to -/// write results and the prompt the operator interacts through. +/// write results and the prompt the user interacts through. pub struct Runner<'i, D: Driver> { program: &'i Program<'i>, appender: Appender, @@ -269,12 +269,12 @@ impl<'i, D: Driver> Runner<'i, D> { let qualified = self .path .render(); - let computable = is_scope_computable(&entry.body); + let kind = self.kind_of_scope(&entry.body); let sealed = match result { Ok(Conclusion::Stopping) => Ok(Conclusion::Stopping), - Ok(Conclusion::Completed(outcome)) => self.seal_scope(&qualified, outcome, computable), + Ok(Conclusion::Completed(outcome)) => self.seal_scope(&qualified, outcome, kind), Ok(Conclusion::Throwing(failure)) => { - self.seal_scope(&qualified, Outcome::Fail(failure), computable) + self.seal_scope(&qualified, Outcome::Fail(failure), kind) } Err(error) => Err(error), }; @@ -357,14 +357,9 @@ impl<'i, D: Driver> Runner<'i, D> { // read-only to confirm. Either way Skip or Fail declines and // records the step; Quit stops. `Pure` builtins just announce // and run. - let nature = match &executable.target { - ExecutableRef::Resolved(id) => self - .library - .nature(*id), - _ => Nature::Pure, - }; + let kind = self.execute_kind(executable); // Pure builtins record nothing; only effectful calls are traced. - let effectful = if let Nature::Pure = nature { + let effectful = if let Kind::Computable = kind { false } else { true @@ -380,8 +375,8 @@ impl<'i, D: Driver> Runner<'i, D> { }, })?; } - let outcome = match nature { - Nature::Command => { + let outcome = match kind { + Kind::System => { let script = self.script_text(env, executable)?; match self .driver @@ -408,7 +403,9 @@ impl<'i, D: Driver> Runner<'i, D> { Err(other) => Err(other), } } - UserInput::Skip => Ok(Conclusion::Completed(Outcome::Skip(Value::Unitus))), + UserInput::Skip => { + Ok(Conclusion::Completed(Outcome::Skip(Value::Unitus))) + } UserInput::Fail(reason) => { Ok(Conclusion::Throwing(Failure::Aborted(reason))) } @@ -418,7 +415,7 @@ impl<'i, D: Driver> Runner<'i, D> { UserInput::Quit => self.record_stop(), } } - Nature::Action => { + Kind::Action => { let (verb, label) = self.action_parts(env, executable)?; match self .driver @@ -434,7 +431,9 @@ impl<'i, D: Driver> Runner<'i, D> { )?; Ok(Conclusion::Completed(Outcome::Done(value))) } - UserInput::Skip => Ok(Conclusion::Completed(Outcome::Skip(Value::Unitus))), + UserInput::Skip => { + Ok(Conclusion::Completed(Outcome::Skip(Value::Unitus))) + } UserInput::Fail(reason) => { Ok(Conclusion::Throwing(Failure::Aborted(reason))) } @@ -444,7 +443,7 @@ impl<'i, D: Driver> Runner<'i, D> { UserInput::Quit => self.record_stop(), } } - Nature::Pure => { + Kind::Computable => { self.driver .announce(&describe_execute(&function)); let value = super::evaluator::dispatch( @@ -456,6 +455,7 @@ impl<'i, D: Driver> Runner<'i, D> { )?; Ok(Conclusion::Completed(Outcome::Done(value))) } + _ => unreachable!(), // execute_kind yields only System/Action/Computable }?; // Pair the Execute with a Return carrying its value; a stopped // run leaves the enter unpaired. @@ -513,7 +513,7 @@ impl<'i, D: Driver> Runner<'i, D> { } } - /// The shell script an `exec` will run, rendered for the operator to see + /// The shell script an `exec` will run, rendered for the user to see /// before they command it. The command's first argument is the script. fn script_text( &mut self, @@ -766,7 +766,7 @@ impl<'i, D: Driver> Runner<'i, D> { // Record the dispatch once the arguments are in hand — // declining at the prompt above returns before this, so a // declined call records no Invoke. Recorded at answer-time, - // so the gap from the previous event is the operator wait. + // so the gap from the previous event is the user wait. let run_id = self .appender .run_id(); @@ -796,14 +796,14 @@ impl<'i, D: Driver> Runner<'i, D> { // then close its scope; a Quit or error skips the close, // leaving the procedure unfinished. let result = self.walk(&mut local, &subroutine.body); - let computable = is_scope_computable(&subroutine.body); + let kind = self.kind_of_scope(&subroutine.body); let sealed = match result { Ok(Conclusion::Stopping) => Ok(Conclusion::Stopping), Ok(Conclusion::Completed(outcome)) => { - self.seal_scope(&lexical, outcome, computable) + self.seal_scope(&lexical, outcome, kind) } Ok(Conclusion::Throwing(failure)) => { - self.seal_scope(&lexical, Outcome::Fail(failure), computable) + self.seal_scope(&lexical, Outcome::Fail(failure), kind) } Err(error) => Err(error), }; @@ -959,7 +959,9 @@ impl<'i, D: Driver> Runner<'i, D> { return Ok(Conclusion::Completed(Outcome::Skip(Value::Unitus))); } UserInput::Fail(reason) => { - return Ok(Conclusion::Completed(Outcome::Fail(Failure::Aborted(reason)))) + return Ok(Conclusion::Completed(Outcome::Fail(Failure::Aborted( + reason, + )))) } UserInput::Override => unreachable!(), // an acquire prompt never offers Override UserInput::Quit => return self.record_stop(), @@ -1166,15 +1168,15 @@ impl<'i, D: Driver> Runner<'i, D> { let result = self.perform_section(env, numeral, title, body); self.path .pop(); - let computable = is_scope_computable(body); + let kind = self.kind_of_scope(body); // A section is a structural scope: the user signs it off at its // close before the next sibling runs. A Quit or error walk skips the // prompt — the section did not complete. match result { Ok(Conclusion::Stopping) => Ok(Conclusion::Stopping), - Ok(Conclusion::Completed(outcome)) => self.seal_scope(&qualified, outcome, computable), + Ok(Conclusion::Completed(outcome)) => self.seal_scope(&qualified, outcome, kind), Ok(Conclusion::Throwing(failure)) => { - self.seal_scope(&qualified, Outcome::Fail(failure), computable) + self.seal_scope(&qualified, Outcome::Fail(failure), kind) } Err(error) => Err(error), } @@ -1214,8 +1216,7 @@ impl<'i, D: Driver> Runner<'i, D> { .. } = op else { - // walk_step called with non-Step operation - unreachable!(); + unreachable!(); // walk_step called with non-Step operation }; for frame in attributes { @@ -1281,18 +1282,8 @@ impl<'i, D: Driver> Runner<'i, D> { if let Conclusion::Stopping = conclusion { return Ok(conclusion); } - // A prologue takes a step's verdict rule: ending in prose, it records - // Skip even when a command within it ran. A thrown failure records Fail - // and still propagates. - let computable = ops - .last() - .is_some_and(is_step_computable); - let conclusion = match conclusion { - Conclusion::Completed(Outcome::Done(value)) if !computable => { - Conclusion::Completed(Outcome::Skip(value)) - } - other => other, - }; + // Translation emits a Prologue only when the description carries real + // work (prose-only descriptions never become step 0) let record = Record { recorded: now_iso8601(), run_id: self @@ -1459,12 +1450,12 @@ impl<'i, D: Driver> Runner<'i, D> { .iter() .map(|r| r.value) .collect(); - let computable = is_step_computable(op); + let kind = self.kind_of_step(op); // `ask` consumes `produced`; keep a copy for a Skip to propagate. let propagate = produced.clone(); let input = self .driver - .ask(qualified, &choices, produced, computable); + .ask(qualified, &choices, produced, kind); // Quit halts the walk; this step's Begin stands without a matching // outcome, so resume re-runs it. @@ -1643,7 +1634,7 @@ impl<'i, D: Driver> Runner<'i, D> { &mut self, qualified: &str, outcome: Outcome, - computable: bool, + kind: Kind, ) -> Result { let standing = match &outcome { Outcome::Fail(_) => Some(Standing::Fail), @@ -1682,7 +1673,7 @@ impl<'i, D: Driver> Runner<'i, D> { .run_id(); let input = self .driver - .seal(qualified, produced, computable); + .seal(qualified, produced, kind); if let UserInput::Quit = input { return self.record_stop(); } @@ -1753,6 +1744,61 @@ impl<'i, D: Driver> Runner<'i, D> { .append(&suspend)?; Ok(Conclusion::Stopping) } + + /// Classify an `Execute` by its builtin's `Nature`, resolving the target as + /// the dispatch does. + fn execute_kind(&self, exec: &Executable) -> Kind { + let nature = match &exec.target { + ExecutableRef::Resolved(id) => self + .library + .nature(*id), + _ => Nature::Pure, + }; + match nature { + Nature::Pure => Kind::Computable, + Nature::Command => Kind::System, + Nature::Action => Kind::Action, + } + } + + /// Classify a step by its final member: a step offering response choices is + /// a `Choice`; otherwise the value comes from the last operation, an ending + /// in prose being `Prose` and everything else `Computable`. + fn kind_of_step(&self, op: &Operation) -> Kind { + match op { + Operation::Step { responses, .. } if !responses.is_empty() => Kind::Choice, + Operation::Step { body, .. } => self.kind_of_step(body), + Operation::Sequence(ops) | Operation::Prologue(ops) => match ops.last() { + Some(last) => self.kind_of_step(last), + None => Kind::Prose, + }, + Operation::Execute(exec) => self.execute_kind(exec), + Operation::Prose(_) => Kind::Prose, + _ => Kind::Computable, + } + } + + /// A scope's Kind: `Computable` if any member holds work, else `Prose`. A + /// pure-prose scope is `Prose`, so an unattended run skips its close. + fn kind_of_scope(&self, op: &Operation) -> Kind { + match op { + Operation::Sequence(ops) | Operation::Prologue(ops) => { + if ops + .iter() + .any(|op| self.kind_of_scope(op) == Kind::Computable) + { + Kind::Computable + } else { + Kind::Prose + } + } + Operation::Step { body, .. } | Operation::Section { body, .. } => { + self.kind_of_scope(body) + } + Operation::Prose(_) => Kind::Prose, + _ => Kind::Computable, + } + } } fn describe_execute(function: &str) -> String { @@ -1825,45 +1871,6 @@ impl Rollup { } } -/// Whether a scope holds any computable work, scanning every member. A scope of -/// pure prose is not computable, so an automatic run skips its close. -fn is_scope_computable(op: &Operation) -> bool { - match op { - Operation::Sequence(ops) | Operation::Prologue(ops) => ops - .iter() - .any(is_scope_computable), - Operation::Step { body, .. } | Operation::Section { body, .. } => is_scope_computable(body), - other => computes(other), - } -} - -/// A single operation's own computability, ignoring nesting: descriptive prose -/// computes nothing; every other primitive (invoke, execute, code, bind, ...) -/// does. The one leaf definition both fold predicates rest on. -fn computes(op: &Operation) -> bool { - match op { - Operation::Prose(_) => false, - _ => true, - } -} - -/// Whether a step computes its result rather than describing it. A step's value -/// is its final member, so one ending in an invocation, code, or substep -/// computes a value while one ending in prose does not. -fn is_step_computable(op: &Operation) -> bool { - match op { - // A step offering response choices needs a selection no non-interactive - // run can make; it is not computable and so is skipped. - Operation::Step { responses, .. } if !responses.is_empty() => false, - Operation::Step { body, .. } => is_step_computable(body), - Operation::Sequence(ops) | Operation::Prologue(ops) => match ops.last() { - Some(last) => is_step_computable(last), - None => false, - }, - other => computes(other), - } -} - /// Lift a `UserInput` from the prompt into a `Conclusion`. An Override records /// `Done ()`, severing the rollup so the failed child below does not propagate /// past this node. A Quit becomes `Stopped`. diff --git a/src/runner/state.rs b/src/runner/state.rs index f6ad8c61..fd0ca0d2 100644 --- a/src/runner/state.rs +++ b/src/runner/state.rs @@ -95,7 +95,7 @@ pub enum InvokeTarget { } /// On-disk store of runs, rooted at some base directory (conventionally -/// `.store/` relative to the operator's current directory). +/// `.store/` relative to the user's current directory). #[allow(dead_code)] pub struct Store { base: PathBuf, @@ -1013,7 +1013,7 @@ fn split_once_top_level_equals(text: &str) -> Option<(&str, &str)> { } // Split `text` once at the first top-level ` ~ ` separator (the binding -// operator joining a supplied value to its parameter name). +// user joining a supplied value to its parameter name). fn split_once_top_level_tilde(text: &str) -> Option<(&str, &str)> { let bytes = text.as_bytes(); let mut depth = 0i32; diff --git a/src/value/types.rs b/src/value/types.rs index 44fd4a33..7c01a74a 100644 --- a/src/value/types.rs +++ b/src/value/types.rs @@ -65,10 +65,9 @@ impl From<&language::Quantity<'_>> for Quantity { } /// Plain-text rendering of a Value, suitable for splicing into an -/// interpolated string fragment or showing the operator in a step -/// description prompt. Numeric formatting delegates to -/// `crate::formatting`'s number renderer so the operator sees the -/// same shape they would in source. +/// interpolated string fragment or showing the user in a step description +/// prompt. Numeric formatting delegates to `crate::formatting`'s number +/// renderer so the user sees the same shape they would in source. /// /// Not intended for on-disk serialization. impl Display for Value { diff --git a/tests/golden/runner/ExecutionInSteps.pfftt b/tests/golden/runner/ExecutionInSteps.pfftt index 88241a96..5fd05e56 100644 --- a/tests/golden/runner/ExecutionInSteps.pfftt +++ b/tests/golden/runner/ExecutionInSteps.pfftt @@ -3,7 +3,7 @@ 2026-06-23T05:57:54.062Z 000088 /local_network:/0 Begin 2026-06-23T05:57:54.062Z 000088 /local_network:/0 Execute exec() 2026-06-23T05:57:55.457Z 000088 /local_network:/0 Return "/usr" -2026-06-23T05:57:55.457Z 000088 /local_network:/0 Skip +2026-06-23T05:57:55.457Z 000088 /local_network:/0 Done () 2026-06-23T05:57:55.457Z 000088 /local_network:/1 Begin 2026-06-23T05:57:55.457Z 000088 /local_network:/1 Execute exec() 2026-06-23T05:57:55.646Z 000088 /local_network:/1 Return "Linux" diff --git a/tests/runner/samples.rs b/tests/runner/samples.rs index 3dd10386..92590ff1 100644 --- a/tests/runner/samples.rs +++ b/tests/runner/samples.rs @@ -118,7 +118,9 @@ fn ensure_run() { // A pure-prose procedure legitimately finishes Skip under the // automatic driver; only a Fail or Stopped run is a test failure. let finished = match outcome { - Conclusion::Completed(Outcome::Done(_)) | Conclusion::Completed(Outcome::Skip(_)) => true, + Conclusion::Completed(Outcome::Done(_)) | Conclusion::Completed(Outcome::Skip(_)) => { + true + } _ => { println!("File {:?} did not finish cleanly: {:?}", file, outcome); false