diff --git a/Cargo.lock b/Cargo.lock index ae7b60d..85bce3b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -81,18 +81,18 @@ checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "clap" -version = "4.5.47" +version = "4.5.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931" +checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.47" +version = "4.5.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6" +checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" dependencies = [ "anstream", "anstyle", @@ -141,12 +141,12 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] @@ -291,9 +291,9 @@ checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" @@ -445,15 +445,15 @@ checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" [[package]] name = "rustix" -version = "1.0.8" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ "bitflags 2.9.4", "errno", "libc", "linux-raw-sys", - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] @@ -464,18 +464,28 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "serde" -version = "1.0.219" +version = "1.0.225" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6c24dee235d0da097043389623fb913daddf92c76e9f5a1db88607a0bcbd1d" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.225" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "659356f9a0cb1e529b24c01e43ad2bdf520ec4ceaf83047b83ddcc2251f96383" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.225" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "0ea936adf78b1f766949a4977b91d2f5595825bd6ec079aa9543ad2685fc4516" dependencies = [ "proc-macro2", "quote", @@ -484,14 +494,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.143" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] @@ -556,7 +567,7 @@ dependencies = [ [[package]] name = "technique" -version = "0.4.3" +version = "0.4.5" dependencies = [ "clap", "lsp-server", @@ -672,9 +683,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "url" @@ -712,6 +723,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-link" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" + [[package]] name = "windows-sys" version = "0.52.0" @@ -730,6 +747,15 @@ dependencies = [ "windows-targets 0.53.3", ] +[[package]] +name = "windows-sys" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa" +dependencies = [ + "windows-link 0.2.0", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -752,7 +778,7 @@ version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ - "windows-link", + "windows-link 0.1.3", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", diff --git a/Cargo.toml b/Cargo.toml index 309d028..a824828 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "technique" -version = "0.4.3" +version = "0.4.5" edition = "2021" description = "A domain specific language for procedures." authors = [ "Andrew Cowie" ] diff --git a/src/editor/server.rs b/src/editor/server.rs index 5781e0c..d7d1923 100644 --- a/src/editor/server.rs +++ b/src/editor/server.rs @@ -487,6 +487,9 @@ impl TechniqueLanguageServer { ParsingError::InvalidSubstep(_, _) => { ("Invalid substep".to_string(), DiagnosticSeverity::ERROR) } + ParsingError::InvalidAttribute(_, _) => { + ("Invalid attribute assignment".to_string(), DiagnosticSeverity::ERROR) + } ParsingError::InvalidResponse(_, _) => { ("Invalid response".to_string(), DiagnosticSeverity::ERROR) } diff --git a/src/formatting/formatter.rs b/src/formatting/formatter.rs index 497392c..9c1ed8f 100644 --- a/src/formatting/formatter.rs +++ b/src/formatting/formatter.rs @@ -165,6 +165,20 @@ pub fn render_procedure_declaration<'i>(procedure: &'i Procedure, renderer: &dyn render_fragments(&sub.fragments, renderer) } +pub fn render_scope<'i>(scope: &'i Scope, renderer: &dyn Render) -> String { + let mut sub = Formatter::new(78); + match scope { + Scope::AttributeBlock { attributes, .. } => { + // Render attributes without indentation for error messages + sub.append_attributes(attributes); + } + _ => { + panic!("Do not use for anything other than rendering Attribute line examples"); + } + } + render_fragments(&sub.fragments, renderer) +} + /// Helper function to convert fragments to a styled string using a renderer fn render_fragments<'i>(fragments: &[(Syntax, Cow<'i, str>)], renderer: &dyn Render) -> String { let mut result = String::new(); @@ -806,6 +820,7 @@ impl<'i> Formatter<'i> { attributes, subscopes, } => { + self.indent(); self.append_attributes(attributes); self.add_fragment_reference(Syntax::Newline, "\n"); @@ -906,7 +921,6 @@ impl<'i> Formatter<'i> { } fn append_attributes(&mut self, attributes: &'i Vec) { - self.indent(); for (i, attribute) in attributes .iter() .enumerate() diff --git a/src/main.rs b/src/main.rs index 8ba376b..b530d5a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -129,6 +129,7 @@ fn main() { .subcommand( Command::new("language") .about("Language Server Protocol integration for editors and IDEs.") + .hide(true) .long_about("Run a Language Server Protocol (LSP) service \ for Technique documents. This accepts commands and code \ input via stdin and returns compilation errors and other \ diff --git a/src/parsing/checks/errors.rs b/src/parsing/checks/errors.rs index d852aff..185de31 100644 --- a/src/parsing/checks/errors.rs +++ b/src/parsing/checks/errors.rs @@ -246,7 +246,7 @@ making_coffee : 1. Do something { re peat() } "# .trim_ascii(), - ParsingError::InvalidFunction(39, 7), + ParsingError::InvalidCodeBlock(39, 10), ); } @@ -259,7 +259,7 @@ making_coffee : 1. Do something { re peat () } "# .trim_ascii(), - ParsingError::InvalidFunction(39, 15), + ParsingError::InvalidCodeBlock(39, 18), ); } diff --git a/src/parsing/parser.rs b/src/parsing/parser.rs index 7487095..42fde0b 100644 --- a/src/parsing/parser.rs +++ b/src/parsing/parser.rs @@ -48,6 +48,7 @@ pub enum ParsingError { InvalidCodeBlock(usize, usize), InvalidStep(usize, usize), InvalidSubstep(usize, usize), + InvalidAttribute(usize, usize), InvalidResponse(usize, usize), InvalidMultiline(usize, usize), InvalidForeach(usize, usize), @@ -87,6 +88,7 @@ impl ParsingError { ParsingError::InvalidMultiline(offset, _) => *offset, ParsingError::InvalidStep(offset, _) => *offset, ParsingError::InvalidSubstep(offset, _) => *offset, + ParsingError::InvalidAttribute(offset, _) => *offset, ParsingError::InvalidForeach(offset, _) => *offset, ParsingError::InvalidResponse(offset, _) => *offset, ParsingError::InvalidIntegral(offset, _) => *offset, @@ -123,6 +125,7 @@ impl ParsingError { ParsingError::InvalidMultiline(_, width) => *width, ParsingError::InvalidStep(_, width) => *width, ParsingError::InvalidSubstep(_, width) => *width, + ParsingError::InvalidAttribute(_, width) => *width, ParsingError::InvalidForeach(_, width) => *width, ParsingError::InvalidResponse(_, width) => *width, ParsingError::InvalidIntegral(_, width) => *width, @@ -1072,14 +1075,20 @@ impl<'i> Parser<'i> { parser.skip_to_next_line(); } } - } else if is_attribute_assignment(content) { - match parser.read_attribute_scope() { - Ok(attribute_block) => elements.push(Element::Steps(vec![attribute_block])), - Err(error) => { - self.problems - .push(error); - parser.skip_to_next_line(); + } else if is_attribute_pattern(content) { + if is_attribute_assignment(content) { + match parser.read_attribute_scope() { + Ok(attribute_block) => elements.push(Element::Steps(vec![attribute_block])), + Err(error) => { + self.problems + .push(error); + parser.skip_to_next_line(); + } } + } else { + self.problems + .push(ParsingError::InvalidAttribute(parser.offset, content.len())); + parser.skip_to_next_line(); } } else if is_step(content) { let mut steps = vec![]; @@ -1124,14 +1133,14 @@ impl<'i> Parser<'i> { && !is_procedure_title(line) && !is_code_block(line) && !malformed_step_pattern(line) - && !is_attribute_assignment(line) + && !is_attribute_pattern(line) }, |line| { is_step(line) || is_procedure_title(line) || is_code_block(line) || malformed_step_pattern(line) - || is_attribute_assignment(line) + || is_attribute_pattern(line) }, |inner| { let content = inner.source; @@ -1452,9 +1461,22 @@ impl<'i> Parser<'i> { .unwrap(); // is_function() already checked let text = &content[0..paren]; - // Validate that the entire text is a valid identifier - let target = validate_identifier(text) - .ok_or(ParsingError::InvalidFunction(self.offset, text.len()))?; + let target = validate_identifier(text); + + if target.is_none() { + if text.contains(' ') || text.contains('<') || text.contains('>') { + let width = if let Some(tilde_pos) = content.find('~') { + tilde_pos.min(content.len()) + } else { + content.len() + }; + return Err(ParsingError::InvalidCodeBlock(self.offset, width)); + } else { + return Err(ParsingError::InvalidFunction(self.offset, text.len())); + } + } + + let target = target.unwrap(); self.advance(text.len()); let parameters = self.read_parameters()?; @@ -1558,7 +1580,25 @@ impl<'i> Parser<'i> { fn read_binding_expression(&mut self) -> Result, ParsingError> { // Parse the expression before the ~ operator - let expression = self.take_until(&['~'], |inner| inner.read_expression())?; + let expression = self.take_until(&['~'], |inner| { + let start_pos = inner.offset; + let expression = inner.read_expression()?; + + // Check for leftover content, erroring if present + inner.trim_whitespace(); + if inner + .source + .is_empty() + { + Ok(expression) + } else { + let width = inner.offset - start_pos + + inner + .source + .len(); + Err(ParsingError::InvalidCodeBlock(start_pos, width)) + } + })?; // Consume the ~ operator self.advance(1); // consume '~' @@ -2130,7 +2170,7 @@ impl<'i> Parser<'i> { || is_substep_dependent(line) || is_substep_parallel(line) || is_subsubstep_dependent(line) - || is_attribute_assignment(line) + || is_attribute_pattern(line) || is_enum_response(line) || malformed_step_pattern(line) || malformed_response_pattern(line) @@ -2463,7 +2503,13 @@ impl<'i> Parser<'i> { ))?; attributes.push(Attribute::Place(identifier)); } else { - return Err(ParsingError::InvalidStep(inner.offset, 0)); + // Check if this looks like a malformed attribute (starts with @ or ^) + if is_attribute_pattern(trimmed) { + // This might be multiple attributes without proper + joiners + return Err(ParsingError::InvalidAttribute(inner.offset, line.len())); + } else { + return Err(ParsingError::InvalidStep(inner.offset, 0)); + } } } @@ -2484,9 +2530,13 @@ impl<'i> Parser<'i> { let content = self.source; - if is_attribute_assignment(content) { - let block = self.read_attribute_scope()?; - scopes.push(block); + if is_attribute_pattern(content) { + if is_attribute_assignment(content) { + let block = self.read_attribute_scope()?; + scopes.push(block); + } else { + return Err(ParsingError::InvalidAttribute(self.offset, content.len())); + } } else if is_substep_dependent(content) { let block = self.read_substep_dependent()?; scopes.push(block); @@ -2791,6 +2841,15 @@ fn potential_procedure_declaration(content: &str) -> bool { .is_empty(); } + // If it's a step patterns then it's not a procedure declaration! + if is_step_dependent(content) + || is_step_parallel(content) + || is_substep_dependent(content) + || is_substep_parallel(content) + { + return false; + } + // Has parentheses -> likely trying to be a procedure with parameters if before.contains('(') { return true; @@ -2997,6 +3056,12 @@ fn is_attribute_assignment(input: &str) -> bool { re.is_match(input) } +/// Detect the beginning of an attribute (role or place) assignment +fn is_attribute_pattern(input: &str) -> bool { + let trimmed = input.trim_ascii_start(); + trimmed.starts_with('@') || trimmed.starts_with('^') +} + // This is a rather monsterous test battery, so we move it into a separate // file. We use the path directive to avoid the need to put it into a // subdirectory with the same name as this module. diff --git a/src/problem/messages.rs b/src/problem/messages.rs index 6c9bed5..09a1ac4 100644 --- a/src/problem/messages.rs +++ b/src/problem/messages.rs @@ -630,6 +630,45 @@ parallel steps, but again this is not compulsory. .trim_ascii() .to_string(), ), + ParsingError::InvalidAttribute(_, _) => { + let examples = vec![ + Scope::AttributeBlock { + attributes: vec![ + Attribute::Role(Identifier("president_of_the_galaxy")), + Attribute::Role(Identifier("femme_fatale")), + ], + subscopes: vec![], + }, + Scope::AttributeBlock { + attributes: vec![ + Attribute::Place(Identifier("milliways")), + Attribute::Role(Identifier("waiter")), + Attribute::Role(Identifier("dish_of_the_day")), + ], + subscopes: vec![], + }, + ]; + + ( + "Invalid attribute assignment".to_string(), + format!( + r#" +Multiple attributes (be they role or place assignments) must be joined using +the '+' operator, for example: + + {} + {} + +Note that an attribute creates a scope, so sub-steps and code blocks can be +nested underneath a role or place assignment. + "#, + examples[0].present(renderer), + examples[1].present(renderer) + ) + .trim_ascii() + .to_string(), + ) + } ParsingError::InvalidForeach(_, _) => { let examples = vec![ Expression::Foreach( diff --git a/src/problem/present.rs b/src/problem/present.rs index ddf7567..c211456 100644 --- a/src/problem/present.rs +++ b/src/problem/present.rs @@ -93,3 +93,9 @@ impl Present for Procedure<'_> { formatter::render_procedure_declaration(self, renderer) } } + +impl Present for Scope<'_> { + fn present(&self, renderer: &dyn Render) -> String { + formatter::render_scope(self, renderer) + } +}