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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions examples/prototype/AirlockPowerdown.tq
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,19 @@ node1_htr_avail_16 :
# Inhibiting Node 1 B HTRS (1 to 6)

@pcs
{ foreach node in seq(1, 6) }
1. Check Availability
2. Perform { cmd("Inhibit") }
3. Check Availability
'Inhibited'
{ foreach node in seq(1, 6) }
1. Check Availability
2. Perform { cmd("Inhibit") }
3. Check Availability
'Inhibited'

node1_htr_avail_79 :

# Inhibiting Node 1 B HTRS (7 to 9)

@pcs
{ foreach node in seq(7, 9) }
1. Check Availability
2. Perform { cmd("Inhibit") }
3. Check Availability
'Inhibited'
{ foreach node in seq(7, 9) }
1. Check Availability
2. Perform { cmd("Inhibit") }
3. Check Availability
'Inhibited'
3 changes: 3 additions & 0 deletions src/domain/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,9 @@ fn render_expression(expr: &Expression) -> String {
Expression::Repeat(inner, _) => {
format!("repeat {}", render_expression(inner))
}
Expression::Within(inner, _) => {
format!("within {}", render_expression(inner))
}
Expression::Foreach(ids, inner, _) => {
let vars = if ids.len() == 1 {
ids[0]
Expand Down
59 changes: 39 additions & 20 deletions src/formatting/formatter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,30 @@ fn is_tablet_list_expr(expr: &Expression) -> bool {
}
}

/// Whether a code block's expressions render as a single inline line
/// (`{ expr }`) rather than the multi-line block form. A `;`-separated
/// sequence and any single non-tablet expression (a value, a control
/// structure like `foreach`/`repeat`/`within`, ...) are inline; a tablet
/// literal is not.
fn is_inline_code_block(expressions: &[Expression]) -> bool {
let has_separator = expressions
.iter()
.any(|e| {
if let Expression::Separator = e {
true
} else {
false
}
});
if has_separator {
true
} else if expressions.len() == 1 {
!is_tablet_list_expr(&expressions[0])
} else {
false
}
}

struct Formatter<'i> {
fragments: Vec<(Syntax, Cow<'i, str>)>,
nesting: u8,
Expand Down Expand Up @@ -1091,8 +1115,13 @@ impl<'i> Formatter<'i> {
return;
}

let is_code = if let Scope::CodeBlock { .. } = subscopes[0] {
true
// A multi-line code block (a tablet literal) stays flush
// with the attribute line, its braces already providing
// visual grouping; a single-line code block — whether a bare
// value/call or a control structure (foreach/repeat/within)
// — gets a normal indent like any other subscope.
let keep_flush = if let Scope::CodeBlock { expressions, .. } = &subscopes[0] {
!is_inline_code_block(expressions)
} else {
false
};
Expand All @@ -1103,7 +1132,7 @@ impl<'i> Formatter<'i> {
self.append_attributes(attributes);
self.add_fragment_reference(Syntax::Newline, "\n");

if !is_code {
if !keep_flush {
self.increase(4);
}
self.append_scope(&subscopes[0]);
Expand All @@ -1113,7 +1142,7 @@ impl<'i> Formatter<'i> {
self.append_scope(scope);
}

if !is_code {
if !keep_flush {
self.decrease(4);
}
}
Expand All @@ -1122,22 +1151,7 @@ impl<'i> Formatter<'i> {
subscopes: substeps,
..
} => {
let has_separator = expressions
.iter()
.any(|e| {
if let Expression::Separator = e {
true
} else {
false
}
});
let inline = if has_separator {
true
} else if expressions.len() == 1 {
!is_tablet_list_expr(&expressions[0])
} else {
false
};
let inline = is_inline_code_block(expressions);

if inline {
self.indent();
Expand Down Expand Up @@ -1308,6 +1322,11 @@ impl<'i> Formatter<'i> {
self.add_fragment_reference(Syntax::Neutral, " ");
self.append_expression(expression);
}
Expression::Within(expression, _) => {
self.add_fragment_reference(Syntax::Keyword, "within");
self.add_fragment_reference(Syntax::Neutral, " ");
self.append_expression(expression);
}
Expression::Foreach(variables, expression, _) => {
self.add_fragment_reference(Syntax::Keyword, "foreach");
self.add_fragment_reference(Syntax::Neutral, " ");
Expand Down
2 changes: 2 additions & 0 deletions src/language/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,7 @@ pub enum Expression<'i> {
Multiline(Option<&'i str>, Vec<&'i str>, Span),
Repeat(Box<Expression<'i>>, Span),
Foreach(Vec<Identifier<'i>>, Box<Expression<'i>>, Span),
Within(Box<Expression<'i>>, Span),
Application(Invocation<'i>, Span),
Execution(Function<'i>, Span),
Binding(Box<Expression<'i>>, Vec<Identifier<'i>>, Span),
Expand All @@ -467,6 +468,7 @@ impl PartialEq for Expression<'_> {
(Expression::Foreach(a1, a2, _), Expression::Foreach(b1, b2, _)) => {
a1 == b1 && a2 == b2
}
(Expression::Within(a, _), Expression::Within(b, _)) => a == b,
(Expression::Application(a, _), Expression::Application(b, _)) => a == b,
(Expression::Execution(a, _), Expression::Execution(b, _)) => a == b,
(Expression::Binding(a1, a2, _), Expression::Binding(b1, b2, _)) => {
Expand Down
4 changes: 4 additions & 0 deletions src/linking/linker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ fn link_operation<'i>(
}
link_operation(body, library, problems);
}
Operation::Within { bound, body, .. } => {
link_operation(bound, library, problems);
link_operation(body, library, problems);
}
Operation::Bind { value, .. } => link_operation(value, library, problems),
Operation::String(fragments) => {
for fragment in fragments {
Expand Down
55 changes: 38 additions & 17 deletions src/parsing/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1144,12 +1144,13 @@ impl<'i> Parser<'i> {
} else if is_code_block(content) {
match parser.read_code_block() {
Ok(expressions) => {
// A loop code block owns the steps below it as its body,
// the way an attribute block owns its scope; `read_scopes`
// stops at a trailing description, leaving it to be flagged.
// A plain code block owns nothing, so following content
// stays at this level.
let subscopes = if is_loop_block(&expressions) {
// A control-structure code block owns the steps below
// it as its body, the way an attribute block owns its
// scope; `read_scopes` stops at a trailing
// description, leaving it to be flagged. A plain code
// block owns nothing, so following content stays at
// this level.
let subscopes = if is_control_structure(&expressions) {
match parser.read_scopes() {
Ok(subscopes) => subscopes,
Err(error) => {
Expand Down Expand Up @@ -1530,6 +1531,8 @@ impl<'i> Parser<'i> {
self.read_repeat_expression()
} else if is_foreach_keyword(content) {
self.read_foreach_expression()
} else if is_within_keyword(content) {
self.read_within_expression()
} else if content.starts_with("foreach ") {
// Malformed foreach expression
return Err(ParsingError::InvalidForeach(Span::new(self.offset, 0)));
Expand Down Expand Up @@ -1746,6 +1749,18 @@ impl<'i> Parser<'i> {
Ok(Expression::Repeat(Box::new(expression), span))
}

fn read_within_expression(&mut self) -> Result<Expression<'i>, ParsingError> {
// Parse "within <expression>"
let start = self.offset;
self.advance(6);
self.trim_whitespace();

let expression = self.read_expression()?;

let span = self.span_since(start);
Ok(Expression::Within(Box::new(expression), span))
}

fn read_binding_expression(&mut self) -> Result<Expression<'i>, ParsingError> {
let start = self.offset;

Expand Down Expand Up @@ -2400,7 +2415,7 @@ impl<'i> Parser<'i> {
|| is_enum_response(line)
|| malformed_step_pattern(line)
|| malformed_response_pattern(line)
|| is_loop_block_line(line)
|| is_control_structure_line(line)
},
|outer| {
let mut results = vec![];
Expand Down Expand Up @@ -3189,29 +3204,29 @@ fn is_code_block(content: &str) -> bool {
re.is_match(content)
}

/// Does this line open a control-structure code block (`{ foreach ... }` or
/// `{ repeat ... }`)? Only a control structure owns the substeps below it and
/// so opens a scope; otherwise a plain expression block (literal value,
/// function call, tablet etc) is inline.
fn is_loop_block_line(content: &str) -> bool {
/// Does this line open a control-structure code block (`{ foreach ... }`,
/// `{ repeat ... }`, or `{ within ... }`)? Only a control structure owns the
/// substeps below it and so opens a scope; otherwise a plain expression block
/// (literal value, function call, tablet etc) is inline.
fn is_control_structure_line(content: &str) -> bool {
if let Some(rest) = content
.trim_ascii_start()
.strip_prefix('{')
{
is_foreach_keyword(rest) || is_repeat_keyword(rest)
is_foreach_keyword(rest) || is_repeat_keyword(rest) || is_within_keyword(rest)
} else {
false
}
}

/// Is this code block is a control structure (`foreach` or `repeat`) which
/// owns the steps below it as its body. A plain code does not.
fn is_loop_block(expressions: &[Expression]) -> bool {
/// Is this code block a control structure (`foreach`, `repeat`, or `within`)
/// which owns the steps below it as its body. A plain code block does not.
fn is_control_structure(expressions: &[Expression]) -> bool {
if expressions.len() != 1 {
return false;
}
match &expressions[0] {
Expression::Foreach(..) | Expression::Repeat(..) => true,
Expression::Foreach(..) | Expression::Repeat(..) | Expression::Within(..) => true,
_ => false,
}
}
Expand All @@ -3236,6 +3251,12 @@ fn is_repeat_keyword(content: &str) -> bool {
re.is_match(content)
}

fn is_within_keyword(content: &str) -> bool {
let re = regex!(r"^\s*within\s+");

re.is_match(content)
}

fn is_unit(content: &str) -> bool {
content.starts_with("()")
}
Expand Down
7 changes: 7 additions & 0 deletions src/program/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,13 @@ pub enum Operation<'i> {
body: Box<Operation<'i>>,
responses: Vec<&'i language::Response<'i>>,
},
/// A `within` block is _not_ generative like `Loop`, but a predicate
/// governing a run-once body.
Within {
bound: Box<Operation<'i>>,
body: Box<Operation<'i>>,
responses: Vec<&'i language::Response<'i>>,
},
Bind {
names: &'i [language::Identifier<'i>],
value: Box<Operation<'i>>,
Expand Down
16 changes: 16 additions & 0 deletions src/resolution/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ fn resolve_operation<'i>(
}
resolve_operation(body, known, arities, problems);
}
Operation::Within { bound, body, .. } => {
resolve_operation(bound, known, arities, problems);
resolve_operation(body, known, arities, problems);
}
Operation::Bind { value, .. } => resolve_operation(value, known, arities, problems),
Operation::Execute(executable) => {
for arg in &mut executable.arguments {
Expand Down Expand Up @@ -177,6 +181,10 @@ fn gather_iterated<'i>(op: &Operation<'i>, iterated: &mut HashSet<&'i str>) {
}
gather_iterated(body, iterated);
}
Operation::Within { bound, body, .. } => {
gather_iterated(bound, iterated);
gather_iterated(body, iterated);
}
Operation::Bind { value, .. } => gather_iterated(value, iterated),
Operation::Sequence(ops)
| Operation::List(ops)
Expand Down Expand Up @@ -243,6 +251,10 @@ fn mark_iterated<'i>(op: &mut Operation<'i>, iterated: &HashSet<&str>) {
}
mark_iterated(body, iterated);
}
Operation::Within { bound, body, .. } => {
mark_iterated(bound, iterated);
mark_iterated(body, iterated);
}
Operation::Sequence(ops)
| Operation::List(ops)
| Operation::Tuple(ops)
Expand Down Expand Up @@ -340,6 +352,10 @@ fn check_scope<'i>(
}
check_scope(body, scope, problems);
}
Operation::Within { bound, body, .. } => {
check_scope(bound, scope, problems);
check_scope(body, scope, problems);
}
Operation::Sequence(ops)
| Operation::List(ops)
| Operation::Tuple(ops)
Expand Down
10 changes: 5 additions & 5 deletions src/runner/checks/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ fn mock_returns_canned_answers_in_order() {
#[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);
p.step("/local_network:I/1", "", "Check the cable.", 1);
let _ = p.ask("/local_network:I/1", &[], Value::Unitus, Kind::Computable);
assert_eq!(
p.events(),
Expand Down Expand Up @@ -63,7 +63,7 @@ fn mock_records_offered_choices() {
#[test]
fn mock_records_enter_and_announce() {
let mut p = Mock::new();
p.enter("I");
p.enter("I", "");
p.announce("Calling helper");
assert_eq!(
p.events(),
Expand All @@ -87,7 +87,7 @@ fn mock_ask_without_answers_panics() {
fn console_step_writes_fqn_and_description() {
let mut output: Vec<u8> = Vec::new();
let mut p = Console::with_output(&mut output);
p.step("local_network:I/1", "Check the cable.", 1);
p.step("local_network:I/1", "", "Check the cable.", 1);
let written = String::from_utf8(output).expect("utf8");
assert!(written.contains("→ local_network:I/1"));
assert!(written.contains(" Check the cable."));
Expand All @@ -99,7 +99,7 @@ fn console_step_indents_description_to_depth() {
let mut p = Console::with_output(&mut output);
// A depth-2 substep indents its prose by eight spaces while the marker
// stays at the left margin.
p.step("local_network:I/1/a", "Inspect the connector.", 2);
p.step("local_network:I/1/a", "", "Inspect the connector.", 2);
let written = String::from_utf8(output).expect("utf8");
assert!(written.contains("→ local_network:I/1/a"));
assert!(written.contains("\n Inspect the connector."));
Expand Down Expand Up @@ -193,7 +193,7 @@ fn console_settle_writes_verdict_line() {
fn console_enter_writes_fqn() {
let mut output: Vec<u8> = Vec::new();
let mut p = Console::with_output(&mut output);
p.enter("I");
p.enter("I", "");
let written = String::from_utf8(output).expect("utf8");
assert!(written.contains("↘ I"));
}
Expand Down
7 changes: 2 additions & 5 deletions src/runner/checks/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2147,11 +2147,8 @@ connectivity_check(e, s, address) :
.unwrap()
.parameters
.unwrap();
let echo = render_argument_echo("connectivity_check:", params, &env);
assert_eq!(
echo,
"connectivity_check: ([] ~ e, 0 ~ s, \"10 Downing Street\" ~ address)"
);
let echo = render_argument_echo(params, &env);
assert_eq!(echo, "([] ~ e, 0 ~ s, \"10 Downing Street\" ~ address)");
}

#[test]
Expand Down
Loading