diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 5fb8f243..7823d1cb 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -9,13 +9,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Clone project - uses: actions/checkout@v3 + uses: actions/checkout@v6 - name: Validate toolchain run: | echo "RUST_VERSION=$(rustc --version | cut -d ' ' -f 2)" >> ${GITHUB_ENV} - - uses: actions/cache@v4 + - uses: actions/cache@v5 with: path: | ~/.cargo/bin/ diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 47f4dc3b..e3260f45 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -16,14 +16,14 @@ jobs: steps: - name: Clone project - uses: actions/checkout@v3 + uses: actions/checkout@v6 - name: Validate toolchain shell: bash run: | echo "RUST_VERSION=$(rustc --version | cut -d ' ' -f 2)" >> ${GITHUB_ENV} - - uses: actions/cache@v4 + - uses: actions/cache@v5 with: path: | ~/.cargo/bin/ diff --git a/Cargo.lock b/Cargo.lock index af5991fb..8515a1c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,9 +13,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -28,15 +28,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -47,7 +47,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -58,7 +58,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -91,18 +91,18 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "clap" -version = "4.5.60" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.60" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -113,15 +113,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "crossbeam-channel" @@ -164,7 +164,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -213,9 +213,9 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "lazy_static" @@ -225,9 +225,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.183" +version = "0.2.184" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" [[package]] name = "linux-raw-sys" @@ -288,14 +288,14 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys", ] [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -372,7 +372,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -472,7 +472,7 @@ dependencies = [ [[package]] name = "technique" -version = "0.5.3" +version = "0.5.4" dependencies = [ "clap", "ignore", @@ -489,12 +489,12 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" +checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", - "windows-sys 0.60.2", + "windows-sys", ] [[package]] @@ -561,9 +561,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -611,7 +611,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -620,15 +620,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets", -] - [[package]] name = "windows-sys" version = "0.61.2" @@ -638,71 +629,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - [[package]] name = "zmij" version = "1.0.21" diff --git a/Cargo.toml b/Cargo.toml index 7cfa01b9..dafdfa49 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "technique" -version = "0.5.3" +version = "0.5.4" edition = "2021" description = "A domain specific language for procedures." authors = [ "Andrew Cowie" ] diff --git a/src/domain/engine.rs b/src/domain/engine.rs index 42b5bed7..95000ac3 100644 --- a/src/domain/engine.rs +++ b/src/domain/engine.rs @@ -154,10 +154,14 @@ impl<'i> Scope<'i> { /// Returns the tablet pairs if this is a CodeBlock containing a Tablet. pub fn tablet(&self) -> Option<&[Pair<'i>]> { match self { - Scope::CodeBlock { expression, .. } => match expression { - Expression::Tablet(pairs) => Some(pairs), - _ => None, - }, + Scope::CodeBlock { expressions, .. } => { + if expressions.len() == 1 { + if let Expression::Tablet(pairs) = &expressions[0] { + return Some(pairs); + } + } + None + } _ => None, } } @@ -189,7 +193,16 @@ impl<'i> Scope<'i> { /// Returns the expression of a CodeBlock as readable text. pub fn expression_text(&self) -> Option { match self { - Scope::CodeBlock { expression, .. } => Some(render_expression(expression)), + Scope::CodeBlock { expressions, .. } => { + if expressions.is_empty() { + return None; + } + let texts: Vec = expressions + .iter() + .map(render_expression) + .collect(); + Some(texts.join("\n")) + } _ => None, } } @@ -331,6 +344,7 @@ fn render_expression(expr: &Expression) -> String { Expression::Number(Numeric::Scientific(q)) => q.to_string(), Expression::Number(Numeric::Integral(n)) => n.to_string(), Expression::Tablet(_) => String::new(), + Expression::Separator => String::new(), } } diff --git a/src/formatting/formatter.rs b/src/formatting/formatter.rs index 51f5906d..74e67ed5 100644 --- a/src/formatting/formatter.rs +++ b/src/formatting/formatter.rs @@ -500,9 +500,18 @@ impl<'i> Formatter<'i> { self.append_char('\n'); // include title in block to keep it with the declaration - let mut elements = procedure.elements.iter(); - if let Some(Element::Title(_)) = procedure.elements.first() { - self.append_element(elements.next().unwrap()); + let mut elements = procedure + .elements + .iter(); + if let Some(Element::Title(_)) = procedure + .elements + .first() + { + self.append_element( + elements + .next() + .unwrap(), + ); } self.add_fragment_reference(Syntax::BlockEnd, ""); @@ -530,14 +539,16 @@ impl<'i> Formatter<'i> { self.add_fragment_reference(Syntax::Newline, "\n"); self.append_steps(steps); } - Element::CodeBlock(expression) => { + Element::CodeBlock(expressions) => { self.add_fragment_reference(Syntax::Structure, "{"); self.add_fragment_reference(Syntax::Newline, "\n"); self.increase(4); - self.indent(); - self.append_expression(expression); - self.add_fragment_reference(Syntax::Newline, "\n"); + for expression in expressions { + self.indent(); + self.append_expression(expression); + self.add_fragment_reference(Syntax::Newline, "\n"); + } self.decrease(4); self.add_fragment_reference(Syntax::Structure, "}"); @@ -821,8 +832,11 @@ impl<'i> Formatter<'i> { return; } - let is_code = - if let Scope::CodeBlock { .. } = subscopes[0] { true } else { false }; + let is_code = if let Scope::CodeBlock { .. } = subscopes[0] { + true + } else { + false + }; // Keep attribute with its first subscope self.add_fragment_reference(Syntax::BlockBegin, ""); @@ -845,31 +859,60 @@ impl<'i> Formatter<'i> { } } Scope::CodeBlock { - expression, + expressions, subscopes: substeps, } => { - match expression { - Expression::Tablet(_) => { - self.indent(); - self.add_fragment_reference(Syntax::Structure, "{"); - self.add_fragment_reference(Syntax::Newline, "\n"); + 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 { + if let Expression::Tablet(_) = &expressions[0] { + false + } else { + true + } + } else { + false + }; - self.increase(4); - self.indent(); - self.append_expression(expression); - self.add_fragment_reference(Syntax::Newline, "\n"); - self.decrease(4); - self.indent(); - self.add_fragment_reference(Syntax::Structure, "}"); + if inline { + self.indent(); + self.add_fragment_reference(Syntax::Structure, "{"); + self.add_fragment_reference(Syntax::Neutral, " "); + for expression in expressions { + if let Expression::Separator = expression { + self.add_fragment_reference(Syntax::Structure, ";"); + self.add_fragment_reference(Syntax::Neutral, " "); + } else { + self.append_expression(expression); + } } - _ => { + self.add_fragment_reference(Syntax::Neutral, " "); + self.add_fragment_reference(Syntax::Structure, "}"); + } else { + self.indent(); + self.add_fragment_reference(Syntax::Structure, "{"); + self.add_fragment_reference(Syntax::Newline, "\n"); + self.increase(4); + for expression in expressions { + if let Expression::Separator = expression { + continue; + } self.indent(); - self.add_fragment_reference(Syntax::Structure, "{"); - self.add_fragment_reference(Syntax::Neutral, " "); self.append_expression(expression); - self.add_fragment_reference(Syntax::Neutral, " "); - self.add_fragment_reference(Syntax::Structure, "}"); + self.add_fragment_reference(Syntax::Newline, "\n"); } + self.decrease(4); + self.indent(); + self.add_fragment_reference(Syntax::Structure, "}"); } self.add_fragment_reference(Syntax::Newline, "\n"); @@ -1022,6 +1065,7 @@ impl<'i> Formatter<'i> { self.append_variables(variables); } Expression::Tablet(pairs) => self.append_tablet(pairs), + Expression::Separator => {} } } diff --git a/src/language/types.rs b/src/language/types.rs index f6937c86..a0ead232 100644 --- a/src/language/types.rs +++ b/src/language/types.rs @@ -40,7 +40,7 @@ pub enum Element<'i> { Title(&'i str), Description(Vec>), Steps(Vec>), - CodeBlock(Expression<'i>), // TODO remove, possibly, if Scope::CodeBlock covers this adequately, or change to Vec as well. + CodeBlock(Vec>), } #[derive(Eq, Debug, PartialEq)] @@ -137,7 +137,7 @@ pub enum Scope<'i> { // Code block scope: { foreach ... } with substeps CodeBlock { - expression: Expression<'i>, + expressions: Vec>, subscopes: Vec>, }, @@ -202,6 +202,7 @@ pub enum Expression<'i> { Execution(Function<'i>), Binding(Box>, Vec>), Tablet(Vec>), + Separator, } #[derive(Debug, PartialEq, Eq)] diff --git a/src/parsing/checks/errors.rs b/src/parsing/checks/errors.rs index 185de316..34fba4f2 100644 --- a/src/parsing/checks/errors.rs +++ b/src/parsing/checks/errors.rs @@ -299,6 +299,6 @@ robot : Your plastic pal who's fun to be with! { re peat } "# .trim_ascii(), - ParsingError::InvalidCodeBlock(50, 7), + ParsingError::InvalidCodeBlock(50, 3), ); } diff --git a/src/parsing/checks/parser.rs b/src/parsing/checks/parser.rs index 0e27ee3d..3baf8d73 100644 --- a/src/parsing/checks/parser.rs +++ b/src/parsing/checks/parser.rs @@ -1087,17 +1087,17 @@ fn code_blocks() { // Test simple identifier in code block input.initialize("{ count }"); let result = input.read_code_block(); - assert_eq!(result, Ok(Expression::Variable(Identifier("count")))); + assert_eq!(result, Ok(vec![Expression::Variable(Identifier("count"))])); // Test function with simple parameter input.initialize("{ sum(count) }"); let result = input.read_code_block(); assert_eq!( result, - Ok(Expression::Execution(Function { + Ok(vec![Expression::Execution(Function { target: Identifier("sum"), parameters: vec![Expression::Variable(Identifier("count"))] - })) + })]) ); // Test function with multiple parameters @@ -1105,14 +1105,14 @@ fn code_blocks() { let result = input.read_code_block(); assert_eq!( result, - Ok(Expression::Execution(Function { + Ok(vec![Expression::Execution(Function { target: Identifier("consume"), parameters: vec![ Expression::Variable(Identifier("apple")), Expression::Variable(Identifier("banana")), Expression::Variable(Identifier("chocolate")) ] - })) + })]) ); // Test function with text parameter @@ -1120,10 +1120,10 @@ fn code_blocks() { let result = input.read_code_block(); assert_eq!( result, - Ok(Expression::Execution(Function { + Ok(vec![Expression::Execution(Function { target: Identifier("exec"), parameters: vec![Expression::String(vec![Piece::Text("Hello, World")])] - })) + })]) ); // Test function with multiline string parameter @@ -1135,13 +1135,13 @@ echo "Done"```) }"#, let result = input.read_code_block(); assert_eq!( result, - Ok(Expression::Execution(Function { + Ok(vec![Expression::Execution(Function { target: Identifier("exec"), parameters: vec![Expression::Multiline( Some("bash"), vec!["ls -l", "echo \"Done\""] )] - })) + })]) ); // Test function with quantity parameter (like timer with duration) @@ -1149,7 +1149,7 @@ echo "Done"```) }"#, let result = input.read_code_block(); assert_eq!( result, - Ok(Expression::Execution(Function { + Ok(vec![Expression::Execution(Function { target: Identifier("timer"), parameters: vec![Expression::Number(Numeric::Scientific(Quantity { mantissa: Decimal { @@ -1160,7 +1160,7 @@ echo "Done"```) }"#, magnitude: None, symbol: "hr" }))] - })) + })]) ); // Test function with integer quantity parameter @@ -1168,10 +1168,10 @@ echo "Done"```) }"#, let result = input.read_code_block(); assert_eq!( result, - Ok(Expression::Execution(Function { + Ok(vec![Expression::Execution(Function { target: Identifier("measure"), parameters: vec![Expression::Number(Numeric::Integral(100))] - })) + })]) ); // Test function with multiple integer parameters @@ -1179,13 +1179,13 @@ echo "Done"```) }"#, let result = input.read_code_block(); assert_eq!( result, - Ok(Expression::Execution(Function { + Ok(vec![Expression::Execution(Function { target: Identifier("seq"), parameters: vec![ Expression::Number(Numeric::Integral(1)), Expression::Number(Numeric::Integral(6)) ] - })) + })]) ); // Test function with decimal quantity parameter @@ -1193,7 +1193,7 @@ echo "Done"```) }"#, let result = input.read_code_block(); assert_eq!( result, - Ok(Expression::Execution(Function { + Ok(vec![Expression::Execution(Function { target: Identifier("wait"), parameters: vec![ Expression::Number(Numeric::Scientific(Quantity { @@ -1207,7 +1207,7 @@ echo "Done"```) }"#, })), Expression::String(vec![Piece::Text("yes")]) ] - })) + })]) ); } @@ -1228,7 +1228,7 @@ fn multiline() { let result = input.read_code_block(); assert_eq!( result, - Ok(Expression::Execution(Function { + Ok(vec![Expression::Execution(Function { target: Identifier("exec"), parameters: vec![Expression::Multiline( Some("bash"), @@ -1241,7 +1241,7 @@ fn multiline() { "fi" ] )] - })) + })]) ); // Test multiline without language tag @@ -1253,10 +1253,10 @@ echo "Done"```) }"#, let result = input.read_code_block(); assert_eq!( result, - Ok(Expression::Execution(Function { + Ok(vec![Expression::Execution(Function { target: Identifier("exec"), parameters: vec![Expression::Multiline(None, vec!["ls -l", "echo \"Done\""])] - })) + })]) ); // Test multiline with intentional empty lines in the middle @@ -1272,7 +1272,7 @@ echo "Ending"```) }"#, let result = input.read_code_block(); assert_eq!( result, - Ok(Expression::Execution(Function { + Ok(vec![Expression::Execution(Function { target: Identifier("exec"), parameters: vec![Expression::Multiline( Some("shell"), @@ -1285,7 +1285,7 @@ echo "Ending"```) }"#, "echo \"Ending\"" ] )] - })) + })]) ); // Test that internal indentation relative to the base is preserved, @@ -1303,7 +1303,7 @@ echo "Ending"```) }"#, let result = input.read_code_block(); assert_eq!( result, - Ok(Expression::Execution(Function { + Ok(vec![Expression::Execution(Function { target: Identifier("exec"), parameters: vec![Expression::Multiline( Some("python"), @@ -1316,7 +1316,7 @@ echo "Ending"```) }"#, "hello()" ] )] - })) + })]) ); // Test that a trailing empty line from the closing delimiter is removed @@ -1328,10 +1328,10 @@ echo test let result = input.read_code_block(); assert_eq!( result, - Ok(Expression::Execution(Function { + Ok(vec![Expression::Execution(Function { target: Identifier("exec"), parameters: vec![Expression::Multiline(None, vec!["echo test"])] - })) + })]) ); // Test various indentation edge cases @@ -1347,7 +1347,7 @@ echo test let result = input.read_code_block(); assert_eq!( result, - Ok(Expression::Execution(Function { + Ok(vec![Expression::Execution(Function { target: Identifier("exec"), parameters: vec![Expression::Multiline( Some("yaml"), @@ -1360,7 +1360,7 @@ echo test " enabled: true" ] )] - })) + })]) ); } @@ -1373,10 +1373,10 @@ fn tablets() { let result = input.read_code_block(); assert_eq!( result, - Ok(Expression::Tablet(vec![Pair { + Ok(vec![Expression::Tablet(vec![Pair { label: "name", value: Expression::String(vec![Piece::Text("Johannes Grammerly")]) - }])) + }])]) ); // Test multiline tablet with string values @@ -1389,7 +1389,7 @@ fn tablets() { let result = input.read_code_block(); assert_eq!( result, - Ok(Expression::Tablet(vec![ + Ok(vec![Expression::Tablet(vec![ Pair { label: "name", value: Expression::String(vec![Piece::Text("Alice of Chains")]) @@ -1398,7 +1398,7 @@ fn tablets() { label: "age", value: Expression::String(vec![Piece::Text("29")]) } - ])) + ])]) ); // Test tablet with mixed value types @@ -1412,7 +1412,7 @@ fn tablets() { let result = input.read_code_block(); assert_eq!( result, - Ok(Expression::Tablet(vec![ + Ok(vec![Expression::Tablet(vec![ Pair { label: "answer", value: Expression::Number(Numeric::Integral(42)) @@ -1428,13 +1428,13 @@ fn tablets() { parameters: vec![] }) } - ])) + ])]) ); // Test empty tablet input.initialize("{ [ ] }"); let result = input.read_code_block(); - assert_eq!(result, Ok(Expression::Tablet(vec![]))); + assert_eq!(result, Ok(vec![Expression::Tablet(vec![])])); // Test tablet with interpolated string values input.initialize( @@ -1446,7 +1446,7 @@ fn tablets() { let result = input.read_code_block(); assert_eq!( result, - Ok(Expression::Tablet(vec![ + Ok(vec![Expression::Tablet(vec![ Pair { label: "context", value: Expression::String(vec![Piece::Text("Details about the thing")]) @@ -1455,7 +1455,7 @@ fn tablets() { label: "status", value: Expression::Variable(Identifier("active")) } - ])) + ])]) ); } @@ -1466,17 +1466,20 @@ fn numeric_literals() { // Test simple integer input.initialize("{ 42 }"); let result = input.read_code_block(); - assert_eq!(result, Ok(Expression::Number(Numeric::Integral(42)))); + assert_eq!(result, Ok(vec![Expression::Number(Numeric::Integral(42))])); // Test negative integer input.initialize("{ -123 }"); let result = input.read_code_block(); - assert_eq!(result, Ok(Expression::Number(Numeric::Integral(-123)))); + assert_eq!( + result, + Ok(vec![Expression::Number(Numeric::Integral(-123))]) + ); // Test zero input.initialize("{ 0 }"); let result = input.read_code_block(); - assert_eq!(result, Ok(Expression::Number(Numeric::Integral(0)))); + assert_eq!(result, Ok(vec![Expression::Number(Numeric::Integral(0))])); } #[test] @@ -1516,10 +1519,10 @@ fn test_foreach_expression() { let result = input.read_code_block(); assert_eq!( result, - Ok(Expression::Foreach( + Ok(vec![Expression::Foreach( vec![Identifier("item")], Box::new(Expression::Variable(Identifier("items"))) - )) + )]) ); } @@ -1531,7 +1534,7 @@ fn foreach_tuple_pattern() { let result = input.read_code_block(); assert_eq!( result, - Ok(Expression::Foreach( + Ok(vec![Expression::Foreach( vec![Identifier("design"), Identifier("component")], Box::new(Expression::Execution(Function { target: Identifier("zip"), @@ -1540,7 +1543,7 @@ fn foreach_tuple_pattern() { Expression::Variable(Identifier("components")) ] })) - )) + )]) ); input.initialize("{ foreach (a, b, c) in zip(list1, list2, list3) }"); @@ -1548,7 +1551,7 @@ fn foreach_tuple_pattern() { let result = input.read_code_block(); assert_eq!( result, - Ok(Expression::Foreach( + Ok(vec![Expression::Foreach( vec![Identifier("a"), Identifier("b"), Identifier("c")], Box::new(Expression::Execution(Function { target: Identifier("zip"), @@ -1558,7 +1561,7 @@ fn foreach_tuple_pattern() { Expression::Variable(Identifier("list3")) ] })) - )) + )]) ); } @@ -1570,13 +1573,13 @@ fn tuple_binding_expression() { let result = input.read_code_block(); assert_eq!( result, - Ok(Expression::Binding( + Ok(vec![Expression::Binding( Box::new(Expression::Application(Invocation { target: Target::Local(Identifier("get_coordinates")), parameters: Some(vec![]) })), vec![Identifier("x"), Identifier("y")] - )) + )]) ); } @@ -1588,9 +1591,9 @@ fn test_repeat_expression() { let result = input.read_code_block(); assert_eq!( result, - Ok(Expression::Repeat(Box::new(Expression::Variable( + Ok(vec![Expression::Repeat(Box::new(Expression::Variable( Identifier("count") - )))) + )))]) ); } @@ -1601,8 +1604,7 @@ fn test_foreach_keyword_boundary() { input.initialize("{ foreachitem in items }"); let result = input.read_code_block(); - // Should fail because "foreachitem" is parsed but "in items" is leftover content - assert_eq!(result, Err(ParsingError::InvalidCodeBlock(2, 11))); + assert_eq!(result, Err(ParsingError::InvalidCodeBlock(2, 12))); } #[test] @@ -1613,7 +1615,10 @@ fn test_repeat_keyword_boundary() { let result = input.read_code_block(); // Should parse as identifier, not repeat - assert_eq!(result, Ok(Expression::Variable(Identifier("repeater")))); + assert_eq!( + result, + Ok(vec![Expression::Variable(Identifier("repeater"))]) + ); } #[test] diff --git a/src/parsing/checks/verify.rs b/src/parsing/checks/verify.rs index 32ff6c93..2e1e5ec1 100644 --- a/src/parsing/checks/verify.rs +++ b/src/parsing/checks/verify.rs @@ -631,10 +631,10 @@ before_leaving : )])], subscopes: vec![Scope::CodeBlock { - expression: Expression::Foreach( + expressions: vec![Expression::Foreach( vec![Identifier("specimen")], Box::new(Expression::Variable(Identifier("specimens"))) - ), + )], subscopes: vec![Scope::AttributeBlock { attributes: vec![Attribute::Role(Identifier( "nursing_team" diff --git a/src/parsing/parser.rs b/src/parsing/parser.rs index ec1cbd71..f4828130 100644 --- a/src/parsing/parser.rs +++ b/src/parsing/parser.rs @@ -1102,7 +1102,7 @@ impl<'i> Parser<'i> { } } else if is_code_block(content) { match parser.read_code_block() { - Ok(expression) => elements.push(Element::CodeBlock(expression)), + Ok(expressions) => elements.push(Element::CodeBlock(expressions)), Err(error) => { self.problems .push(error); @@ -1395,57 +1395,49 @@ impl<'i> Parser<'i> { Ok((numeral, title)) } - fn read_code_block(&mut self) -> Result, ParsingError> { + fn read_code_block(&mut self) -> Result>, ParsingError> { self.take_block_chars("a code block", '{', '}', true, |inner| { - // Save the start position (accounting for leading whitespace that read_expression will trim) - inner.trim_whitespace(); - let start = inner.offset; - - let expression = inner.read_expression()?; + let mut expressions = Vec::new(); - // Check if there's leftover content - let offset_before_trim = inner.offset; - inner.trim_whitespace(); - if !inner - .source - .is_empty() - { - let mut width = offset_before_trim - start; // Width of what we parsed + loop { + inner.trim_whitespace(); + if inner + .source + .is_empty() + { + break; + } + let start = inner.offset; + let expression = inner.read_expression()?; + let is_variable = if let Expression::Variable(_) = &expression { + true + } else { + false + }; + expressions.push(expression); - // Check if leftover looks like continuation of identifier - let leftover = inner + inner.trim_whitespace(); + if inner .source - .chars() - .next() - .map(|ch| { - ch.is_ascii_lowercase() - && !inner - .source - .starts_with("in ") - }) - .unwrap_or(false); - - if leftover { - // Include the space(s) between parts - width = inner.offset - start; - - // Add identifier-like characters from leftover - for ch in inner + .starts_with(';') + { + inner.advance(1); + expressions.push(Expression::Separator); + } else if is_variable + && !inner .source - .chars() - { - if ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '_' { - width += ch.len_utf8(); - } else { - break; - } - } + .is_empty() + { + let width = inner.offset - start; + return Err(ParsingError::InvalidCodeBlock(start, width)); } + } - return Err(ParsingError::InvalidCodeBlock(start, width)); + if expressions.is_empty() { + return Err(ParsingError::InvalidCodeBlock(inner.offset, 0)); } - Ok(expression) + Ok(expressions) }) } @@ -1519,6 +1511,12 @@ impl<'i> Parser<'i> { Ok(Expression::Execution(function)) } else { let identifier = self.read_identifier()?; + if self.source.starts_with('"') { + return Err(ParsingError::InvalidFunction( + self.offset - identifier.0.len(), + identifier.0.len(), + )); + } Ok(Expression::Variable(identifier)) } } @@ -1784,7 +1782,7 @@ impl<'i> Parser<'i> { let content = self.source; - let possible = match content.find([' ', '\t', '\n', '(', '{', ',']) { + let possible = match content.find([' ', '\t', '\n', '(', '{', ',', '"']) { None => content, Some(i) => &content[0..i], }; @@ -2223,8 +2221,13 @@ impl<'i> Parser<'i> { // standalone CodeBlock wrapped in a Paragraph // FIXME this needs to be promoted to a Scope::CodeBlock? Or better yet shouldnt' be here? - let code_block = outer.read_code_block()?; - results.push(Paragraph(vec![Descriptive::CodeInline(code_block)])); + let expressions = outer.read_code_block()?; + for expr in expressions { + if let Expression::Separator = expr { + continue; + } + results.push(Paragraph(vec![Descriptive::CodeInline(expr)])); + } } else { // Paragraph container let descriptives = outer.take_paragraph(|parser| { @@ -2237,8 +2240,13 @@ impl<'i> Parser<'i> { } if c == '{' { - let expression = parser.read_code_block()?; - content.push(Descriptive::CodeInline(expression)); + let expressions = parser.read_code_block()?; + for expr in expressions { + if let Expression::Separator = expr { + continue; + } + content.push(Descriptive::CodeInline(expr)); + } } else if parser .source .starts_with("```") @@ -2637,11 +2645,11 @@ impl<'i> Parser<'i> { false // Never stop - consume all remaining content }, |outer| { - let code = outer.read_code_block()?; + let expressions = outer.read_code_block()?; let subscopes = outer.read_scopes()?; Ok(Scope::CodeBlock { - expression: code, + expressions, subscopes, }) }, diff --git a/tests/formatting/formatter.rs b/tests/formatting/formatter.rs index 938e59c8..61fd1c8b 100644 --- a/tests/formatting/formatter.rs +++ b/tests/formatting/formatter.rs @@ -185,10 +185,10 @@ win_le_tour : Bicycle -> YellowJersey name: Identifier("vibe_coding"), parameters: None, signature: None, - elements: vec![Element::CodeBlock(Expression::Execution(Function { + elements: vec![Element::CodeBlock(vec![Expression::Execution(Function { target: Identifier("exec"), parameters: vec![Expression::Multiline(Some("bash"), vec!["rm -rf /"])], - }))], + })])], }])), }; @@ -281,7 +281,7 @@ We must take action! subscopes: vec![Scope::AttributeBlock { attributes: vec![Attribute::Role(Identifier("journalist"))], subscopes: vec![Scope::CodeBlock { - expression: Expression::Tablet(vec![ + expressions: vec![Expression::Tablet(vec![ Pair { label: "timestamp", value: Expression::Execution(Function { @@ -293,7 +293,7 @@ We must take action! label: "message", value: Expression::Variable(Identifier("msg")), }, - ]), + ])], subscopes: vec![], }], }], @@ -359,10 +359,10 @@ Record everything, with timestamps. "Specimen labelling", )])], subscopes: vec![Scope::CodeBlock { - expression: Expression::Foreach( + expressions: vec![Expression::Foreach( vec![Identifier("specimen")], Box::new(Expression::Variable(Identifier("specimens"))), - ), + )], subscopes: vec![Scope::AttributeBlock { attributes: vec![Attribute::Role(Identifier( "nursing_team", diff --git a/tests/samples/Sequence.tq b/tests/samples/Sequence.tq new file mode 100644 index 00000000..d0a9f8bb --- /dev/null +++ b/tests/samples/Sequence.tq @@ -0,0 +1,16 @@ +delete_rds_instance : + +# Destroy RDS Database + +Destroy an RDS database instance. As with EC2, before we can get rid of an +instance we have to disable termination protection. + + 1. Disable termination protection + { + click("Modify") + navigate("bottom") + deselect("Enable deletion protection") + click("Continue") + select("Apply Immediately") + click("Modify DB instance") + }