diff --git a/.gitignore b/.gitignore index 33c0e46e..d14796c8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /.vscode /target +/plans # rendering artifacts *.pdf diff --git a/src/domain/engine.rs b/src/domain/engine.rs index 95000ac3..32c797a7 100644 --- a/src/domain/engine.rs +++ b/src/domain/engine.rs @@ -45,7 +45,7 @@ impl<'i> Procedure<'i> { self.elements .iter() .flat_map(|element| match element { - Element::Steps(steps) => steps.iter(), + Element::Steps(steps, _) => steps.iter(), _ => [].iter(), }) } @@ -55,7 +55,7 @@ impl<'i> Procedure<'i> { self.elements .iter() .flat_map(|element| match element { - Element::Description(paragraphs) => paragraphs.iter(), + Element::Description(paragraphs, _) => paragraphs.iter(), _ => [].iter(), }) } @@ -115,7 +115,7 @@ impl<'i> Scope<'i> { /// Returns an iterator over responses if this is a ResponseBlock. pub fn responses(&self) -> impl Iterator> { let slice: &[Response<'i>] = match self { - Scope::ResponseBlock { responses } => responses, + Scope::ResponseBlock { responses, .. } => responses, _ => &[], }; slice.iter() @@ -127,7 +127,7 @@ impl<'i> Scope<'i> { Scope::AttributeBlock { attributes, .. } => attributes .iter() .filter_map(|attr| match attr { - Attribute::Role(id) => Some(id.0), + Attribute::Role(id, _) => Some(id.value), _ => None, }) .collect::>() @@ -142,7 +142,7 @@ impl<'i> Scope<'i> { Scope::AttributeBlock { attributes, .. } => attributes .iter() .filter_map(|attr| match attr { - Attribute::Place(id) => Some(id.0), + Attribute::Place(id, _) => Some(id.value), _ => None, }) .collect::>() @@ -156,7 +156,7 @@ impl<'i> Scope<'i> { match self { Scope::CodeBlock { expressions, .. } => { if expressions.len() == 1 { - if let Expression::Tablet(pairs) = &expressions[0] { + if let Expression::Tablet(pairs, _) = &expressions[0] { return Some(pairs); } } @@ -232,7 +232,7 @@ impl<'i> Procedure<'i> { /// Returns the procedure name. pub fn name(&self) -> &'i str { self.name - .0 + .value } } @@ -252,10 +252,10 @@ impl<'i> Response<'i> { /// Returns (expression_text, body_lines) where body_lines captures multiline /// content separately for distinct styling. fn render_expression_parts(expr: &Expression) -> (String, Vec) { - if let Expression::Execution(func) = expr { + if let Expression::Execution(func, _) = expr { let mut body = Vec::new(); for param in &func.parameters { - if let Expression::Multiline(_, lines) = param { + if let Expression::Multiline(_, lines, _) = param { body.extend( lines .iter() @@ -268,7 +268,7 @@ fn render_expression_parts(expr: &Expression) -> (String, Vec) { format!( "{}(", func.target - .0 + .value ), body, ); @@ -279,29 +279,29 @@ fn render_expression_parts(expr: &Expression) -> (String, Vec) { fn render_expression(expr: &Expression) -> String { match expr { - Expression::Repeat(inner) => { + Expression::Repeat(inner, _) => { format!("repeat {}", render_expression(inner)) } - Expression::Foreach(ids, inner) => { + Expression::Foreach(ids, inner, _) => { let vars = if ids.len() == 1 { ids[0] - .0 + .value .to_string() } else { format!( "({})", ids.iter() - .map(|id| id.0) + .map(|id| id.value) .collect::>() .join(", ") ) }; format!("foreach {} in {}", vars, render_expression(inner)) } - Expression::Application(inv) => { + Expression::Application(inv, _) => { let name = match &inv.target { - Target::Local(id) => id.0, - Target::Remote(ext) => ext.0, + Target::Local(id) => id.value, + Target::Remote(ext) => ext.value, }; if let Some(params) = &inv.parameters { let args: Vec<_> = params @@ -313,7 +313,7 @@ fn render_expression(expr: &Expression) -> String { format!("<{}>", name) } } - Expression::Execution(func) => { + Expression::Execution(func, _) => { let args: Vec<_> = func .parameters .iter() @@ -322,16 +322,16 @@ fn render_expression(expr: &Expression) -> String { format!( "{}({})", func.target - .0, + .value, args.join(", ") ) } - Expression::Multiline(_, lines) => lines.join("\n"), - Expression::Variable(id) => { - id.0.to_string() - } - Expression::Binding(inner, _) => render_expression(inner), - Expression::String(pieces) => { + Expression::Multiline(_, lines, _) => lines.join("\n"), + Expression::Variable(id, _) => id + .value + .to_string(), + Expression::Binding(inner, _, _) => render_expression(inner), + Expression::String(pieces, _) => { let mut result = String::new(); for piece in pieces { match piece { @@ -341,9 +341,9 @@ fn render_expression(expr: &Expression) -> String { } result } - Expression::Number(Numeric::Scientific(q)) => q.to_string(), - Expression::Number(Numeric::Integral(n)) => n.to_string(), - Expression::Tablet(_) => String::new(), + Expression::Number(Numeric::Scientific(q), _) => q.to_string(), + Expression::Number(Numeric::Integral(n), _) => n.to_string(), + Expression::Tablet(_, _) => String::new(), Expression::Separator => String::new(), } } @@ -411,16 +411,16 @@ impl<'i> Paragraph<'i> { fn expression_content(expr: &crate::language::Expression<'i>) -> String { match expr { - crate::language::Expression::Application(invocation) => { + crate::language::Expression::Application(invocation, _) => { Self::invocation_name(invocation).to_string() } - crate::language::Expression::Repeat(inner) => { + crate::language::Expression::Repeat(inner, _) => { format!("repeat {}", Self::expression_content(inner)) } - crate::language::Expression::Foreach(_, inner) => { + crate::language::Expression::Foreach(_, inner, _) => { format!("foreach {}", Self::expression_content(inner)) } - crate::language::Expression::Binding(inner, _) => Self::expression_content(inner), + crate::language::Expression::Binding(inner, _, _) => Self::expression_content(inner), _ => String::new(), } } @@ -458,16 +458,16 @@ impl<'i> Paragraph<'i> { expr: &crate::language::Expression<'i>, ) { match expr { - crate::language::Expression::Application(inv) => { + crate::language::Expression::Application(inv, _) => { targets.push(Self::invocation_name(inv)); } - crate::language::Expression::Repeat(inner) => { + crate::language::Expression::Repeat(inner, _) => { Self::extract_expression_invocations(targets, inner); } - crate::language::Expression::Foreach(_, inner) => { + crate::language::Expression::Foreach(_, inner, _) => { Self::extract_expression_invocations(targets, inner); } - crate::language::Expression::Binding(inner, _) => { + crate::language::Expression::Binding(inner, _, _) => { Self::extract_expression_invocations(targets, inner); } _ => {} @@ -476,8 +476,8 @@ impl<'i> Paragraph<'i> { fn invocation_name(inv: &crate::language::Invocation<'i>) -> &'i str { match &inv.target { - crate::language::Target::Local(id) => id.0, - crate::language::Target::Remote(ext) => ext.0, + crate::language::Target::Local(id) => id.value, + crate::language::Target::Remote(ext) => ext.value, } } } @@ -543,11 +543,13 @@ impl Prose { #[cfg(test)] mod check { - use crate::language::{Descriptive, Expression, Identifier, Invocation, Paragraph, Target}; + use crate::language::{ + Descriptive, Expression, Identifier, Invocation, Paragraph, Span, Target, + }; fn local<'a>(name: &'a str) -> Invocation<'a> { Invocation { - target: Target::Local(Identifier(name)), + target: Target::Local(Identifier::new(name)), parameters: None, } } @@ -555,7 +557,7 @@ mod check { // Pure text: "Ensure physical and digital safety" #[test] fn text_only_paragraph() { - let p = Paragraph(vec![Descriptive::Text( + let p = Paragraph::new(vec![Descriptive::Text( "Ensure physical and digital safety", )]); assert_eq!(p.text(), "Ensure physical and digital safety"); @@ -568,7 +570,7 @@ mod check { // Bare invocation: #[test] fn invocation_only_paragraph() { - let p = Paragraph(vec![Descriptive::Application(local("ensure_safety"))]); + let p = Paragraph::new(vec![Descriptive::Application(local("ensure_safety"))]); assert_eq!(p.text(), ""); assert_eq!(p.invocations(), vec!["ensure_safety"]); assert_eq!(p.content(), "ensure_safety"); @@ -578,7 +580,7 @@ mod check { // Text is present so content() returns just the text. #[test] fn mixed_text_and_invocation() { - let p = Paragraph(vec![ + let p = Paragraph::new(vec![ Descriptive::Text("Define Requirements"), Descriptive::Application(local("define_requirements")), ]); @@ -590,9 +592,13 @@ mod check { // CodeInline with repeat: { repeat } #[test] fn repeat_expression() { - let p = Paragraph(vec![Descriptive::CodeInline(Expression::Repeat(Box::new( - Expression::Application(local("incident_action_cycle")), - )))]); + let p = Paragraph::new(vec![Descriptive::CodeInline(Expression::Repeat( + Box::new(Expression::Application( + local("incident_action_cycle"), + Span::default(), + )), + Span::default(), + ))]); assert_eq!(p.text(), ""); assert_eq!(p.invocations(), vec!["incident_action_cycle"]); assert_eq!(p.content(), "repeat incident_action_cycle"); @@ -601,9 +607,9 @@ mod check { // Binding wrapping an invocation: (s) ~ e #[test] fn binding_with_invocation() { - let p = Paragraph(vec![Descriptive::Binding( + let p = Paragraph::new(vec![Descriptive::Binding( Box::new(Descriptive::Application(local("observe"))), - vec![Identifier("e")], + vec![Identifier::new("e")], )]); assert_eq!(p.text(), ""); assert_eq!(p.invocations(), vec!["observe"]); @@ -613,9 +619,10 @@ mod check { // CodeInline with foreach: { foreach design in designs } #[test] fn foreach_expression() { - let p = Paragraph(vec![Descriptive::CodeInline(Expression::Foreach( - vec![Identifier("design")], - Box::new(Expression::Application(local("implement"))), + let p = Paragraph::new(vec![Descriptive::CodeInline(Expression::Foreach( + vec![Identifier::new("design")], + Box::new(Expression::Application(local("implement"), Span::default())), + Span::default(), ))]); assert_eq!(p.text(), ""); assert_eq!(p.invocations(), vec!["implement"]); diff --git a/src/domain/nasa_esa_iss/adapter.rs b/src/domain/nasa_esa_iss/adapter.rs index 630cecce..3880643f 100644 --- a/src/domain/nasa_esa_iss/adapter.rs +++ b/src/domain/nasa_esa_iss/adapter.rs @@ -113,7 +113,9 @@ fn rewrite_expression(expr: &str) -> String { if let Some(rest) = expr.strip_prefix("foreach ") { if let Some((var, seq_expr)) = rest.split_once(" in ") { if let Some((start, end)) = parse_seq(seq_expr) { - let values: Vec = (start..=end).map(|n| n.to_string()).collect(); + let values: Vec = (start..=end) + .map(|n| n.to_string()) + .collect(); return format!("foreach {} {}", var.trim(), values.join(" ")); } } @@ -123,7 +125,16 @@ fn rewrite_expression(expr: &str) -> String { /// Parse `seq(A, B)` into a (start, end) pair. fn parse_seq(s: &str) -> Option<(i64, i64)> { - let inner = s.strip_prefix("seq(")?.strip_suffix(')')?; + let inner = s + .strip_prefix("seq(")? + .strip_suffix(')')?; let (a, b) = inner.split_once(", ")?; - Some((a.trim().parse().ok()?, b.trim().parse().ok()?)) + Some(( + a.trim() + .parse() + .ok()?, + b.trim() + .parse() + .ok()?, + )) } diff --git a/src/domain/recipe/adapter.rs b/src/domain/recipe/adapter.rs index cf123ce0..eed669e5 100644 --- a/src/domain/recipe/adapter.rs +++ b/src/domain/recipe/adapter.rs @@ -254,8 +254,8 @@ fn strip_ordinals(steps: &mut Vec) { /// Format an expression value as a human-readable quantity string. fn format_value(expr: &language::Expression) -> String { match expr { - language::Expression::Number(language::Numeric::Scientific(q)) => q.to_string(), - language::Expression::Number(language::Numeric::Integral(n)) => n.to_string(), + language::Expression::Number(language::Numeric::Scientific(q), _) => q.to_string(), + language::Expression::Number(language::Numeric::Integral(n), _) => n.to_string(), _ => String::new(), } } @@ -304,20 +304,20 @@ fn builtin_from_descriptive(d: &language::Descriptive) -> Option { fn builtin_from_expression(expr: &language::Expression) -> Option { match expr { - language::Expression::Application(inv) => builtin_from_invocation(inv), - language::Expression::Execution(func) => builtin_suffix( + language::Expression::Application(inv, _) => builtin_from_invocation(inv), + language::Expression::Execution(func, _) => builtin_suffix( func.target - .0, + .value, &func.parameters, ), - language::Expression::Binding(inner, _) => builtin_from_expression(inner), + language::Expression::Binding(inner, _, _) => builtin_from_expression(inner), _ => None, } } fn builtin_from_invocation(inv: &language::Invocation) -> Option { let name = match &inv.target { - language::Target::Local(id) => id.0, + language::Target::Local(id) => id.value, _ => return None, }; match &inv.parameters { diff --git a/src/domain/source/adapter.rs b/src/domain/source/adapter.rs index 8e997e84..1efba76c 100644 --- a/src/domain/source/adapter.rs +++ b/src/domain/source/adapter.rs @@ -48,11 +48,13 @@ fn coalesce(fragments: Vec) -> Vec { && frag.syntax != "BlockBegin" && frag.syntax != "BlockEnd" { - last.content.push_str(&frag.content); + last.content + .push_str(&frag.content); continue; } if is_text_whitespace(&frag) { - last.content.push_str(&frag.content); + last.content + .push_str(&frag.content); continue; } } @@ -64,10 +66,11 @@ fn coalesce(fragments: Vec) -> Vec { fn is_text_whitespace(frag: &Fragment) -> bool { (frag.syntax == "Neutral" || frag.syntax == "Description") - && !frag.content.is_empty() + && !frag + .content + .is_empty() && frag .content .bytes() .all(|b| b == b' ') } - diff --git a/src/domain/source/typst.rs b/src/domain/source/typst.rs index d956c07c..8aae3ce5 100644 --- a/src/domain/source/typst.rs +++ b/src/domain/source/typst.rs @@ -55,11 +55,7 @@ impl Render for Fragment { } else if self.syntax == "Newline" { out.raw(&format!("#{}()\n", func)); } else { - out.raw(&format!( - "#{}(\"{}\")", - func, - escape_string(&self.content) - )); + out.raw(&format!("#{}(\"{}\")", func, escape_string(&self.content))); } } } diff --git a/src/editor/server.rs b/src/editor/server.rs index 996df2c4..c17df3b6 100644 --- a/src/editor/server.rs +++ b/src/editor/server.rs @@ -499,7 +499,7 @@ impl TechniqueLanguageServer { let name = procedure .name - .0; + .value; // Calculate the byte offset of the name using pointer arithmetic let offset = calculate_slice_offset(content, name).unwrap_or(0); @@ -701,123 +701,123 @@ impl TechniqueLanguageServer { }; let (message, severity) = match &error { - ParsingError::IllegalParserState(_, _) => ( + ParsingError::IllegalParserState(_) => ( "Internal parser error".to_string(), DiagnosticSeverity::ERROR, ), - ParsingError::Unimplemented(_, _) => ( + ParsingError::Unimplemented(_) => ( "Unimplemented feature".to_string(), DiagnosticSeverity::WARNING, ), - ParsingError::Unrecognized(_, _) => { + ParsingError::Unrecognized(_) => { ("Unrecognized syntax".to_string(), DiagnosticSeverity::ERROR) } - ParsingError::UnexpectedEndOfInput(_, _) => ( + ParsingError::UnexpectedEndOfInput(_) => ( "Unexpected end of input".to_string(), DiagnosticSeverity::ERROR, ), - ParsingError::Expected(_, _, expected) => { + ParsingError::Expected(_, expected) => { (format!("Expected {}", expected), DiagnosticSeverity::ERROR) } - ParsingError::ExpectedMatchingChar(_, _, subject, start, end) => ( + ParsingError::ExpectedMatchingChar(_, subject, start, end) => ( format!("Expected matching '{}' for '{}' in {}", end, start, subject), DiagnosticSeverity::ERROR, ), - ParsingError::MissingParenthesis(_, _) => ( + ParsingError::MissingParenthesis(_) => ( "Require parenthesis around multiple parameters in binding".to_string(), DiagnosticSeverity::ERROR, ), - ParsingError::InvalidCharacter(_, _, ch) => ( + ParsingError::InvalidCharacter(_, ch) => ( format!("Invalid character '{}'", ch), DiagnosticSeverity::ERROR, ), - ParsingError::InvalidHeader(_, _) => { + ParsingError::InvalidHeader(_) => { ("Invalid header line".to_string(), DiagnosticSeverity::ERROR) } - ParsingError::InvalidIdentifier(_, _, id) => ( + ParsingError::InvalidIdentifier(_, id) => ( format!("Invalid identifier '{}'", id), DiagnosticSeverity::ERROR, ), - ParsingError::InvalidForma(_, _) => ( + ParsingError::InvalidForma(_) => ( "Invalid forma in signature".to_string(), DiagnosticSeverity::ERROR, ), - ParsingError::InvalidGenus(_, _) => ( + ParsingError::InvalidGenus(_) => ( "Invalid genus in signature".to_string(), DiagnosticSeverity::ERROR, ), - ParsingError::InvalidSignature(_, _) => ( + ParsingError::InvalidSignature(_) => ( "Invalid signature in procedure declaration".to_string(), DiagnosticSeverity::ERROR, ), - ParsingError::InvalidParameters(_, _) => ( + ParsingError::InvalidParameters(_) => ( "Malformed parameters in procedure declaration".to_string(), DiagnosticSeverity::ERROR, ), - ParsingError::InvalidDeclaration(_, _) => ( + ParsingError::InvalidDeclaration(_) => ( "Invalid procedure declaration".to_string(), DiagnosticSeverity::ERROR, ), - ParsingError::InvalidSection(_, _) => ( + ParsingError::InvalidSection(_) => ( "Invalid section heading".to_string(), DiagnosticSeverity::ERROR, ), - ParsingError::InvalidInvocation(_, _) => ( + ParsingError::InvalidInvocation(_) => ( "Invalid procedure Invocation".to_string(), DiagnosticSeverity::ERROR, ), - ParsingError::InvalidFunction(_, _) => ( + ParsingError::InvalidFunction(_) => ( "Invalid function call".to_string(), DiagnosticSeverity::ERROR, ), - ParsingError::InvalidCodeBlock(_, _) => { + ParsingError::InvalidCodeBlock(_) => { ("Invalid code block".to_string(), DiagnosticSeverity::ERROR) } - ParsingError::InvalidStep(_, _) => { + ParsingError::InvalidStep(_) => { ("Invalid step".to_string(), DiagnosticSeverity::ERROR) } - ParsingError::InvalidSubstep(_, _) => { + ParsingError::InvalidSubstep(_) => { ("Invalid substep".to_string(), DiagnosticSeverity::ERROR) } - ParsingError::InvalidAttribute(_, _) => ( + ParsingError::InvalidAttribute(_) => ( "Invalid attribute assignment".to_string(), DiagnosticSeverity::ERROR, ), - ParsingError::InvalidResponse(_, _) => { + ParsingError::InvalidResponse(_) => { ("Invalid response".to_string(), DiagnosticSeverity::ERROR) } - ParsingError::InvalidMultiline(_, _) => ( + ParsingError::InvalidMultiline(_) => ( "Invalid multiline content".to_string(), DiagnosticSeverity::ERROR, ), - ParsingError::InvalidForeach(_, _) => ( + ParsingError::InvalidForeach(_) => ( "Invalid foreach expression".to_string(), DiagnosticSeverity::ERROR, ), - ParsingError::InvalidIntegral(_, _) => ( + ParsingError::InvalidIntegral(_) => ( "Invalid integral number".to_string(), DiagnosticSeverity::ERROR, ), - ParsingError::InvalidQuantity(_, _) => { + ParsingError::InvalidQuantity(_) => { ("Invalid quantity".to_string(), DiagnosticSeverity::ERROR) } - ParsingError::InvalidQuantityDecimal(_, _) => ( + ParsingError::InvalidQuantityDecimal(_) => ( "Invalid quantity decimal".to_string(), DiagnosticSeverity::ERROR, ), - ParsingError::InvalidQuantityUncertainty(_, _) => ( + ParsingError::InvalidQuantityUncertainty(_) => ( "Invalid quantity uncertainty".to_string(), DiagnosticSeverity::ERROR, ), - ParsingError::InvalidQuantityMagnitude(_, _) => ( + ParsingError::InvalidQuantityMagnitude(_) => ( "Invalid quantity magnitude".to_string(), DiagnosticSeverity::ERROR, ), - ParsingError::InvalidQuantitySymbol(_, _) => ( + ParsingError::InvalidQuantitySymbol(_) => ( "Invalid quantity symbol".to_string(), DiagnosticSeverity::ERROR, ), - ParsingError::UnclosedInterpolation(_, _) => ( + ParsingError::UnclosedInterpolation(_) => ( "Unclosed interpolation".to_string(), DiagnosticSeverity::ERROR, ), @@ -869,6 +869,7 @@ fn offset_to_position(text: &str, offset: usize) -> Position { #[cfg(test)] mod tests { use super::*; + use technique::language::Span; #[test] fn test_calculate_str_offsets() { @@ -936,17 +937,17 @@ mod tests { fn test_parsing_error_types() { // Test that all error types can be converted to messages without panicking let test_errors = vec![ - ParsingError::IllegalParserState(0, 0), - ParsingError::Unimplemented(0, 0), - ParsingError::Unrecognized(0, 0), - ParsingError::UnexpectedEndOfInput(0, 0), - ParsingError::Expected(0, 0, "test"), - ParsingError::ExpectedMatchingChar(0, 0, "test", '(', ')'), - ParsingError::MissingParenthesis(0, 0), - ParsingError::InvalidCharacter(0, 0, 'x'), - ParsingError::InvalidHeader(0, 0), - ParsingError::InvalidIdentifier(0, 0, "test".to_string()), - ParsingError::InvalidDeclaration(0, 0), + ParsingError::IllegalParserState(Span::new(0, 0)), + ParsingError::Unimplemented(Span::new(0, 0)), + ParsingError::Unrecognized(Span::new(0, 0)), + ParsingError::UnexpectedEndOfInput(Span::new(0, 0)), + ParsingError::Expected(Span::new(0, 0), "test"), + ParsingError::ExpectedMatchingChar(Span::new(0, 0), "test", '(', ')'), + ParsingError::MissingParenthesis(Span::new(0, 0)), + ParsingError::InvalidCharacter(Span::new(0, 0), 'x'), + ParsingError::InvalidHeader(Span::new(0, 0)), + ParsingError::InvalidIdentifier(Span::new(0, 0), "test".to_string()), + ParsingError::InvalidDeclaration(Span::new(0, 0)), ]; // This shouldn't panic - just test that all enum variants are handled @@ -956,25 +957,25 @@ mod tests { // Test message generation (this was formerly in convert_parsing_errors) match &error { - ParsingError::IllegalParserState(_, _) => { + ParsingError::IllegalParserState(_) => { assert_eq!("Internal parser error", "Internal parser error") } - ParsingError::Unimplemented(_, _) => { + ParsingError::Unimplemented(_) => { assert_eq!("Unimplemented feature", "Unimplemented feature") } - ParsingError::Unrecognized(_, _) => { + ParsingError::Unrecognized(_) => { assert_eq!("Unrecognized syntax", "Unrecognized syntax") } - ParsingError::UnexpectedEndOfInput(_, _) => { + ParsingError::UnexpectedEndOfInput(_) => { assert_eq!("Unexpected end of input", "Unexpected end of input") } - ParsingError::Expected(_, _, expected) => assert_eq!(*expected, "test"), - ParsingError::ExpectedMatchingChar(_, _, subject, start, end) => { + ParsingError::Expected(_, expected) => assert_eq!(*expected, "test"), + ParsingError::ExpectedMatchingChar(_, subject, start, end) => { assert_eq!(*subject, "test"); assert_eq!(*start, '('); assert_eq!(*end, ')'); } - ParsingError::InvalidDeclaration(_, _) => { + ParsingError::InvalidDeclaration(_) => { assert_eq!("Invalid declaration", "Invalid declaration") } _ => {} // Other variants tested implicitly diff --git a/src/formatting/formatter.rs b/src/formatting/formatter.rs index 74e67ed5..8d5c8902 100644 --- a/src/formatting/formatter.rs +++ b/src/formatting/formatter.rs @@ -94,7 +94,7 @@ pub fn render_forma<'i>(forma: &'i Forma, renderer: &dyn Render) -> String { } pub fn render_identifier<'i>(identifier: &'i Identifier, renderer: &dyn Render) -> String { - renderer.style(Syntax::Declaration, identifier.0) + renderer.style(Syntax::Declaration, identifier.value) } pub fn render_response<'i>(response: &'i Response, renderer: &dyn Render) -> String { @@ -151,7 +151,7 @@ pub fn render_procedure_declaration<'i>(procedure: &'i Procedure, renderer: &dyn Syntax::Declaration, procedure .name - .0, + .value, ); if let Some(parameters) = &procedure.parameters { sub.append_parameters(parameters); @@ -330,7 +330,7 @@ impl<'i> Formatter<'i> { fn render_inline_code(&self, expr: &'i Expression) -> Vec<(Syntax, Cow<'i, str>)> { match expr { - Expression::Tablet(_) | Expression::Multiline(_, _) => { + Expression::Tablet(_, _) | Expression::Multiline(_, _, _) => { // These are not inline, caller should handle specially Vec::new() } @@ -478,7 +478,7 @@ impl<'i> Formatter<'i> { self.add_fragment_reference(Syntax::BlockBegin, ""); let name = &procedure.name; - self.add_fragment_reference(Syntax::Declaration, name.0); + self.add_fragment_reference(Syntax::Declaration, name.value); if let Some(parameters) = &procedure.parameters { // note that append_arguments() is for general expression @@ -503,7 +503,7 @@ impl<'i> Formatter<'i> { let mut elements = procedure .elements .iter(); - if let Some(Element::Title(_)) = procedure + if let Some(Element::Title(_, _)) = procedure .elements .first() { @@ -525,21 +525,21 @@ impl<'i> Formatter<'i> { fn append_element(&mut self, element: &'i Element) { match element { - Element::Title(title) => { + Element::Title(title, _) => { self.add_fragment_reference(Syntax::Newline, "\n"); self.add_fragment_reference(Syntax::Header, "# "); self.add_fragment_reference(Syntax::Title, title); self.add_fragment_reference(Syntax::Newline, "\n"); } - Element::Description(paragraphs) => { + Element::Description(paragraphs, _) => { self.add_fragment_reference(Syntax::Newline, "\n"); self.append_paragraphs(paragraphs); } - Element::Steps(steps) => { + Element::Steps(steps, _) => { self.add_fragment_reference(Syntax::Newline, "\n"); self.append_steps(steps); } - Element::CodeBlock(expressions) => { + Element::CodeBlock(expressions, _) => { self.add_fragment_reference(Syntax::Structure, "{"); self.add_fragment_reference(Syntax::Newline, "\n"); @@ -615,13 +615,13 @@ impl<'i> Formatter<'i> { self.add_fragment_reference(Syntax::Structure, ","); self.add_fragment_reference(Syntax::Neutral, " "); } - self.add_fragment_reference(Syntax::Variable, variable.0); + self.add_fragment_reference(Syntax::Variable, variable.value); } self.add_fragment_reference(Syntax::Structure, ")"); } pub fn append_forma(&mut self, forma: &'i Forma) { - self.add_fragment_reference(Syntax::Forma, forma.0) + self.add_fragment_reference(Syntax::Forma, forma.value) } fn append_paragraphs(&mut self, paragraphs: &'i Vec) { @@ -657,7 +657,7 @@ impl<'i> Formatter<'i> { line.add_breakable(syntax, text); } Descriptive::CodeInline(expr) => match expr { - Expression::Tablet(_) => { + Expression::Tablet(_, _) => { line.flush(); self.add_fragment_reference(Syntax::Structure, "{"); self.append_char('\n'); @@ -669,7 +669,7 @@ impl<'i> Formatter<'i> { line = self.builder(); line.add_word(Syntax::Structure, "}"); } - Expression::Multiline(_, _) => { + Expression::Multiline(_, _, _) => { line.flush(); self.add_fragment_reference(Syntax::Structure, "{"); self.increase(4); @@ -681,11 +681,11 @@ impl<'i> Formatter<'i> { line.add_word(Syntax::Structure, "}"); } _ => match expr { - Expression::Execution(func) + Expression::Execution(func, _) if func .parameters .iter() - .any(|p| matches!(p, Expression::Multiline(_, _))) => + .any(|p| matches!(p, Expression::Multiline(_, _, _))) => { line.flush(); self.add_fragment_reference(Syntax::Neutral, " "); @@ -747,6 +747,7 @@ impl<'i> Formatter<'i> { ordinal, description: content, subscopes: scopes, + .. } => { self.indent(); self.add_fragment_string(Syntax::StepItem, format!("{}.", ordinal)); @@ -771,6 +772,7 @@ impl<'i> Formatter<'i> { bullet, description, subscopes, + .. } => { self.indent(); self.add_fragment_string(Syntax::StepItem, bullet.to_string()); @@ -824,6 +826,7 @@ impl<'i> Formatter<'i> { Scope::AttributeBlock { attributes, subscopes, + .. } => { if subscopes.len() == 0 { self.indent(); @@ -861,6 +864,7 @@ impl<'i> Formatter<'i> { Scope::CodeBlock { expressions, subscopes: substeps, + .. } => { let has_separator = expressions .iter() @@ -874,7 +878,7 @@ impl<'i> Formatter<'i> { let inline = if has_separator { true } else if expressions.len() == 1 { - if let Expression::Tablet(_) = &expressions[0] { + if let Expression::Tablet(_, _) = &expressions[0] { false } else { true @@ -921,7 +925,7 @@ impl<'i> Formatter<'i> { self.append_scopes(substeps); self.decrease(4); } - Scope::ResponseBlock { responses } => { + Scope::ResponseBlock { responses, .. } => { self.increase(4); self.indent(); self.append_responses(responses); @@ -931,6 +935,7 @@ impl<'i> Formatter<'i> { numeral, title, body, + .. } => { self.add_fragment_reference(Syntax::StepItem, numeral); self.add_fragment_reference(Syntax::Structure, "."); @@ -974,13 +979,13 @@ impl<'i> Formatter<'i> { self.add_fragment_reference(Syntax::Neutral, " + "); } match attribute { - Attribute::Role(name) => { + Attribute::Role(name, _) => { self.add_fragment_reference(Syntax::Attribute, "@"); - self.add_fragment_reference(Syntax::Attribute, name.0); + self.add_fragment_reference(Syntax::Attribute, name.value); } - Attribute::Place(name) => { + Attribute::Place(name, _) => { self.add_fragment_reference(Syntax::Attribute, "^"); - self.add_fragment_reference(Syntax::Attribute, name.0); + self.add_fragment_reference(Syntax::Attribute, name.value); } } } @@ -988,10 +993,10 @@ impl<'i> Formatter<'i> { pub fn append_expression(&mut self, expression: &'i Expression) { match expression { - Expression::Variable(identifier) => { - self.add_fragment_reference(Syntax::Variable, identifier.0); + Expression::Variable(identifier, _) => { + self.add_fragment_reference(Syntax::Variable, identifier.value); } - Expression::String(pieces) => { + Expression::String(pieces, _) => { self.add_fragment_reference(Syntax::Quote, "\""); for piece in pieces { match piece { @@ -1009,8 +1014,8 @@ impl<'i> Formatter<'i> { } self.add_fragment_reference(Syntax::Quote, "\""); } - Expression::Number(numeric) => self.append_numeric(numeric), - Expression::Multiline(lang, lines) => { + Expression::Number(numeric, _) => self.append_numeric(numeric), + Expression::Multiline(lang, lines, _) => { self.append_char('\n'); self.indent(); @@ -1041,12 +1046,12 @@ impl<'i> Formatter<'i> { self.add_fragment_reference(Syntax::Quote, "```"); self.append_char('\n'); } - Expression::Repeat(expression) => { + Expression::Repeat(expression, _) => { self.add_fragment_reference(Syntax::Keyword, "repeat"); self.add_fragment_reference(Syntax::Neutral, " "); self.append_expression(expression); } - Expression::Foreach(variables, expression) => { + Expression::Foreach(variables, expression, _) => { self.add_fragment_reference(Syntax::Keyword, "foreach"); self.add_fragment_reference(Syntax::Neutral, " "); self.append_variables(variables); @@ -1055,16 +1060,16 @@ impl<'i> Formatter<'i> { self.add_fragment_reference(Syntax::Neutral, " "); self.append_expression(expression); } - Expression::Application(invocation) => self.append_application(invocation), - Expression::Execution(function) => self.append_function(function), - Expression::Binding(expression, variables) => { + Expression::Application(invocation, _) => self.append_application(invocation), + Expression::Execution(function, _) => self.append_function(function), + Expression::Binding(expression, variables, _) => { self.append_expression(expression); self.add_fragment_reference(Syntax::Neutral, " "); self.add_fragment_reference(Syntax::Structure, "~"); self.add_fragment_reference(Syntax::Neutral, " "); self.append_variables(variables); } - Expression::Tablet(pairs) => self.append_tablet(pairs), + Expression::Tablet(pairs, _) => self.append_tablet(pairs), Expression::Separator => {} } } @@ -1083,7 +1088,7 @@ impl<'i> Formatter<'i> { self.add_fragment_reference(Syntax::Structure, ","); self.add_fragment_reference(Syntax::Neutral, " "); } - self.add_fragment_reference(Syntax::Variable, variable.0); + self.add_fragment_reference(Syntax::Variable, variable.value); } if variables.len() > 1 { self.add_fragment_reference(Syntax::Structure, ")"); @@ -1127,9 +1132,11 @@ impl<'i> Formatter<'i> { self.add_fragment_reference(Syntax::Quote, "<"); match &invocation.target { Target::Local(identifier) => { - self.add_fragment_reference(Syntax::Invocation, identifier.0) + self.add_fragment_reference(Syntax::Invocation, identifier.value) + } + Target::Remote(external) => { + self.add_fragment_reference(Syntax::Invocation, external.value) } - Target::Remote(external) => self.add_fragment_reference(Syntax::Invocation, external.0), } self.add_fragment_reference(Syntax::Quote, ">"); if let Some(parameters) = &invocation.parameters { @@ -1162,13 +1169,13 @@ impl<'i> Formatter<'i> { Syntax::Function, function .target - .0, + .value, ); self.add_fragment_reference(Syntax::Structure, "("); let mut has_multiline = false; for parameter in &function.parameters { - if let Expression::Multiline(_, _) = parameter { + if let Expression::Multiline(_, _, _) = parameter { has_multiline = true; break; } @@ -1408,7 +1415,8 @@ mod check { fn genus() { let mut output = Formatter::new(78); - output.append_forma(&Forma("Jedi")); + let forma = Forma::new("Jedi"); + output.append_forma(&forma); assert_eq!(output.to_string(), "Jedi"); output.reset(); @@ -1416,19 +1424,21 @@ mod check { assert_eq!(output.to_string(), "()"); output.reset(); - output.append_genus(&Genus::Single(Forma("Stormtrooper"))); + let single = Genus::Single(Forma::new("Stormtrooper")); + output.append_genus(&single); assert_eq!(output.to_string(), "Stormtrooper"); output.reset(); - output.append_genus(&Genus::List(Forma("Pilot"))); + let list = Genus::List(Forma::new("Pilot")); + output.append_genus(&list); assert_eq!(output.to_string(), "[Pilot]"); output.reset(); let genus = Genus::Tuple(vec![ - Forma("Kid"), - Forma("Pilot"), - Forma("Scoundrel"), - Forma("Princess"), + Forma::new("Kid"), + Forma::new("Pilot"), + Forma::new("Scoundrel"), + Forma::new("Princess"), ]); output.append_genus(&genus); assert_eq!(output.to_string(), "(Kid, Pilot, Scoundrel, Princess)"); @@ -1440,23 +1450,25 @@ mod check { fn signatures() { let mut output = Formatter::new(78); - output.append_signature(&Signature { - requires: Genus::Single(Forma("Alderaan")), - provides: Genus::Single(Forma("AsteroidField")), - }); + let sig = Signature { + requires: Genus::Single(Forma::new("Alderaan")), + provides: Genus::Single(Forma::new("AsteroidField")), + }; + output.append_signature(&sig); assert_eq!(output.to_string(), "Alderaan -> AsteroidField"); output.reset(); - output.append_signature(&Signature { - requires: Genus::List(Forma("Clone")), - provides: Genus::Single(Forma("Army")), - }); + let sig = Signature { + requires: Genus::List(Forma::new("Clone")), + provides: Genus::Single(Forma::new("Army")), + }; + output.append_signature(&sig); assert_eq!(output.to_string(), "[Clone] -> Army"); output.reset(); let signature = Signature { - requires: Genus::Single(Forma("TaxationOfTradeRoutes")), - provides: Genus::Tuple(vec![Forma("Rebels"), Forma("Empire")]), + requires: Genus::Single(Forma::new("TaxationOfTradeRoutes")), + provides: Genus::Tuple(vec![Forma::new("Rebels"), Forma::new("Empire")]), }; output.append_signature(&signature); assert_eq!( diff --git a/src/language/types.rs b/src/language/types.rs index a0ead232..7df1fc6d 100644 --- a/src/language/types.rs +++ b/src/language/types.rs @@ -2,6 +2,19 @@ use crate::regex::*; +/// Byte range within the original source. `length` excludes trailing whitespace. +#[derive(Copy, Clone, Default, Eq, Debug, PartialEq, PartialOrd, Ord)] +pub struct Span { + pub offset: usize, + pub length: usize, +} + +impl Span { + pub const fn new(offset: usize, length: usize) -> Self { + Span { offset, length } + } +} + #[derive(Eq, Debug, PartialEq)] pub struct Document<'i> { pub source: Option<&'i str>, @@ -9,12 +22,22 @@ pub struct Document<'i> { pub body: Option>, } -#[derive(Eq, Debug, PartialEq)] +#[derive(Eq, Debug)] pub struct Metadata<'i> { pub version: u8, pub license: Option<&'i str>, pub copyright: Option<&'i str>, pub domain: Option<&'i str>, + pub span: Span, +} + +impl PartialEq for Metadata<'_> { + fn eq(&self, other: &Self) -> bool { + self.version == other.version + && self.license == other.license + && self.copyright == other.copyright + && self.domain == other.domain + } } impl Default for Metadata<'_> { @@ -24,6 +47,7 @@ impl Default for Metadata<'_> { license: None, copyright: None, domain: None, + span: Span::default(), } } } @@ -35,20 +59,42 @@ pub enum Technique<'i> { Empty, } -#[derive(Eq, Debug, PartialEq)] +#[derive(Eq, Debug)] pub enum Element<'i> { - Title(&'i str), - Description(Vec>), - Steps(Vec>), - CodeBlock(Vec>), + Title(&'i str, Span), + Description(Vec>, Span), + Steps(Vec>, Span), + CodeBlock(Vec>, Span), } -#[derive(Eq, Debug, PartialEq)] +impl PartialEq for Element<'_> { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Element::Title(a, _), Element::Title(b, _)) => a == b, + (Element::Description(a, _), Element::Description(b, _)) => a == b, + (Element::Steps(a, _), Element::Steps(b, _)) => a == b, + (Element::CodeBlock(a, _), Element::CodeBlock(b, _)) => a == b, + _ => false, + } + } +} + +#[derive(Eq, Debug)] pub struct Procedure<'i> { pub name: Identifier<'i>, pub parameters: Option>>, pub signature: Option>, pub elements: Vec>, + pub span: Span, +} + +impl PartialEq for Procedure<'_> { + fn eq(&self, other: &Self) -> bool { + self.name == other.name + && self.parameters == other.parameters + && self.signature == other.signature + && self.elements == other.elements + } } impl<'i> Procedure<'i> { @@ -56,17 +102,55 @@ impl<'i> Procedure<'i> { self.elements .iter() .find_map(|element| match element { - Element::Title(title) => return Some(*title), + Element::Title(title, _) => return Some(*title), _ => None, }) } } -#[derive(Eq, Debug, PartialEq)] -pub struct Identifier<'i>(pub &'i str); +#[derive(Eq, Debug)] +pub struct Identifier<'i> { + pub value: &'i str, + pub span: Span, +} -#[derive(Eq, Debug, PartialEq)] -pub struct External<'i>(pub &'i str); +impl PartialEq for Identifier<'_> { + fn eq(&self, other: &Self) -> bool { + self.value == other.value + } +} + +impl<'i> Identifier<'i> { + /// Test helper: builds an `Identifier` with a default span. See also the + /// `PartialEq` instance. + pub const fn new(value: &'i str) -> Self { + Identifier { + value, + span: Span::new(0, 0), + } + } +} + +#[derive(Eq, Debug)] +pub struct External<'i> { + pub value: &'i str, + pub span: Span, +} + +impl PartialEq for External<'_> { + fn eq(&self, other: &Self) -> bool { + self.value == other.value + } +} + +impl<'i> External<'i> { + pub const fn new(value: &'i str) -> Self { + External { + value, + span: Span::new(0, 0), + } + } +} #[derive(Eq, Debug, PartialEq)] pub enum Target<'i> { @@ -74,8 +158,26 @@ pub enum Target<'i> { Remote(External<'i>), } -#[derive(Eq, Debug, PartialEq)] -pub struct Forma<'i>(pub &'i str); +#[derive(Eq, Debug)] +pub struct Forma<'i> { + pub value: &'i str, + pub span: Span, +} + +impl PartialEq for Forma<'_> { + fn eq(&self, other: &Self) -> bool { + self.value == other.value + } +} + +impl<'i> Forma<'i> { + pub const fn new(value: &'i str) -> Self { + Forma { + value, + span: Span::new(0, 0), + } + } +} #[derive(Eq, Debug, PartialEq)] pub enum Genus<'i> { @@ -102,8 +204,20 @@ pub struct Invocation<'i> { // types for descriptive content -#[derive(Eq, Debug, PartialEq)] -pub struct Paragraph<'i>(pub Vec>); +#[derive(Eq, Debug)] +pub struct Paragraph<'i>(pub Vec>, pub Span); + +impl PartialEq for Paragraph<'_> { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl<'i> Paragraph<'i> { + pub fn new(descriptives: Vec>) -> Self { + Paragraph(descriptives, Span::default()) + } +} #[derive(Eq, Debug, PartialEq)] pub enum Descriptive<'i> { @@ -115,35 +229,40 @@ pub enum Descriptive<'i> { // types for Steps within procedures -#[derive(Eq, Debug, PartialEq)] +#[derive(Eq, Debug)] pub enum Scope<'i> { DependentBlock { ordinal: &'i str, description: Vec>, subscopes: Vec>, + span: Span, }, ParallelBlock { bullet: char, description: Vec>, subscopes: Vec>, + span: Span, }, // Attribute scope: @role (or other attributes) with substeps AttributeBlock { attributes: Vec>, subscopes: Vec>, + span: Span, }, // Code block scope: { foreach ... } with substeps CodeBlock { expressions: Vec>, subscopes: Vec>, + span: Span, }, // Response block scope: 'Yes' | 'No' responses ResponseBlock { responses: Vec>, + span: Span, }, // Section chunk scope: organizational container with technique content @@ -151,23 +270,119 @@ pub enum Scope<'i> { numeral: &'i str, title: Option>, body: Technique<'i>, + span: Span, }, } +impl PartialEq for Scope<'_> { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + ( + Scope::DependentBlock { + ordinal: a1, + description: a2, + subscopes: a3, + .. + }, + Scope::DependentBlock { + ordinal: b1, + description: b2, + subscopes: b3, + .. + }, + ) => a1 == b1 && a2 == b2 && a3 == b3, + ( + Scope::ParallelBlock { + bullet: a1, + description: a2, + subscopes: a3, + .. + }, + Scope::ParallelBlock { + bullet: b1, + description: b2, + subscopes: b3, + .. + }, + ) => a1 == b1 && a2 == b2 && a3 == b3, + ( + Scope::AttributeBlock { + attributes: a1, + subscopes: a2, + .. + }, + Scope::AttributeBlock { + attributes: b1, + subscopes: b2, + .. + }, + ) => a1 == b1 && a2 == b2, + ( + Scope::CodeBlock { + expressions: a1, + subscopes: a2, + .. + }, + Scope::CodeBlock { + expressions: b1, + subscopes: b2, + .. + }, + ) => a1 == b1 && a2 == b2, + ( + Scope::ResponseBlock { responses: a, .. }, + Scope::ResponseBlock { responses: b, .. }, + ) => a == b, + ( + Scope::SectionChunk { + numeral: a1, + title: a2, + body: a3, + .. + }, + Scope::SectionChunk { + numeral: b1, + title: b2, + body: b3, + .. + }, + ) => a1 == b1 && a2 == b2 && a3 == b3, + _ => false, + } + } +} + // enum responses like 'Yes' | 'No' -#[derive(Eq, Debug, PartialEq)] +#[derive(Eq, Debug)] pub struct Response<'i> { pub value: &'i str, pub condition: Option<&'i str>, + pub span: Span, +} + +impl PartialEq for Response<'_> { + fn eq(&self, other: &Self) -> bool { + self.value == other.value && self.condition == other.condition + } } // attributes like @chef -#[derive(Eq, Debug, PartialEq)] +#[derive(Eq, Debug)] pub enum Attribute<'i> { - Role(Identifier<'i>), - Place(Identifier<'i>), + Role(Identifier<'i>, Span), + Place(Identifier<'i>, Span), +} + +impl PartialEq for Attribute<'_> { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Attribute::Role(a, _), Attribute::Role(b, _)) => a == b, + (Attribute::Place(a, _), Attribute::Place(b, _)) => a == b, + _ => false, + } + } } // now types used within code blocks @@ -190,21 +405,46 @@ pub enum Piece<'i> { Interpolation(Expression<'i>), } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, Eq)] pub enum Expression<'i> { - Variable(Identifier<'i>), - String(Vec>), - Number(Numeric<'i>), - Multiline(Option<&'i str>, Vec<&'i str>), - Repeat(Box>), - Foreach(Vec>, Box>), - Application(Invocation<'i>), - Execution(Function<'i>), - Binding(Box>, Vec>), - Tablet(Vec>), + Variable(Identifier<'i>, Span), + String(Vec>, Span), + Number(Numeric<'i>, Span), + Multiline(Option<&'i str>, Vec<&'i str>, Span), + Repeat(Box>, Span), + Foreach(Vec>, Box>, Span), + Application(Invocation<'i>, Span), + Execution(Function<'i>, Span), + Binding(Box>, Vec>, Span), + Tablet(Vec>, Span), Separator, } +impl PartialEq for Expression<'_> { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Expression::Variable(a, _), Expression::Variable(b, _)) => a == b, + (Expression::String(a, _), Expression::String(b, _)) => a == b, + (Expression::Number(a, _), Expression::Number(b, _)) => a == b, + (Expression::Multiline(a1, a2, _), Expression::Multiline(b1, b2, _)) => { + a1 == b1 && a2 == b2 + } + (Expression::Repeat(a, _), Expression::Repeat(b, _)) => a == b, + (Expression::Foreach(a1, a2, _), Expression::Foreach(b1, b2, _)) => { + a1 == b1 && a2 == b2 + } + (Expression::Application(a, _), Expression::Application(b, _)) => a == b, + (Expression::Execution(a, _), Expression::Execution(b, _)) => a == b, + (Expression::Binding(a1, a2, _), Expression::Binding(b1, b2, _)) => { + a1 == b1 && a2 == b2 + } + (Expression::Tablet(a, _), Expression::Tablet(b, _)) => a == b, + (Expression::Separator, Expression::Separator) => true, + _ => false, + } + } +} + #[derive(Debug, PartialEq, Eq)] pub enum Numeric<'i> { Integral(i64), @@ -246,20 +486,20 @@ pub(crate) fn validate_domain(input: &str) -> Option<&str> { } } -pub(crate) fn validate_identifier(input: &str) -> Option> { +pub(crate) fn validate_identifier(input: &str, span: Span) -> Option> { if input.len() == 0 { return None; } let re = regex!(r"^[a-z][a-z0-9_]*$"); if re.is_match(input) { - Some(Identifier(input)) + Some(Identifier { value: input, span }) } else { None } } -pub(crate) fn validate_forma(input: &str) -> Option> { +pub(crate) fn validate_forma(input: &str, span: Span) -> Option> { if input.len() == 0 { return None; } @@ -280,15 +520,21 @@ pub(crate) fn validate_forma(input: &str) -> Option> { } } - Some(Forma(input)) + Some(Forma { value: input, span }) +} + +/// `child` must be a sub-slice of `parent`. +fn sub_span(parent: &str, child: &str, parent_span: Span) -> Span { + let inner = (child.as_ptr() as usize) - (parent.as_ptr() as usize); + Span::new(parent_span.offset + inner, child.len()) } -fn parse_tuple(input: &str) -> Option>> { +fn parse_tuple(input: &str, span: Span) -> Option>> { let mut formas: Vec = Vec::new(); for text in input.split(",") { let text = text.trim_ascii(); - let forma = validate_forma(text)?; + let forma = validate_forma(text, sub_span(input, text, span))?; formas.push(forma); } @@ -296,7 +542,7 @@ fn parse_tuple(input: &str) -> Option>> { } /// This one copes with (and discards) any internal whitespace encountered. -pub(crate) fn validate_genus(input: &str) -> Option> { +pub(crate) fn validate_genus(input: &str, span: Span) -> Option> { let first = input .chars() .next() @@ -315,7 +561,7 @@ pub(crate) fn validate_genus(input: &str) -> Option> { return None; } - let forma = validate_forma(content)?; + let forma = validate_forma(content, sub_span(input, content, span))?; Some(Genus::List(forma)) } @@ -331,7 +577,7 @@ pub(crate) fn validate_genus(input: &str) -> Option> { return Some(Genus::Unit); } - let formas = parse_tuple(content)?; + let formas = parse_tuple(content, sub_span(input, content, span))?; Some(Genus::Tuple(formas)) } _ => { @@ -341,10 +587,10 @@ pub(crate) fn validate_genus(input: &str) -> Option> { // Check if this is a bare tuple (comma-separated but non-parenthesized) if input.contains(',') { - let formas = parse_tuple(input)?; + let formas = parse_tuple(input, span)?; Some(Genus::Naked(formas)) } else { - let forma = validate_forma(input)?; + let forma = validate_forma(input, span)?; Some(Genus::Single(forma)) } } @@ -370,7 +616,11 @@ pub fn validate_response(input: &str) -> Option> { None => None, }; - Some(Response { value, condition }) + Some(Response { + value, + condition, + span: Span::default(), + }) } #[cfg(test)] @@ -379,144 +629,160 @@ mod check { #[test] fn identifier_rules() { - assert_eq!(validate_identifier("a"), Some(Identifier("a"))); - assert_eq!(validate_identifier("ab"), Some(Identifier("ab"))); - assert_eq!(validate_identifier("johnny5"), Some(Identifier("johnny5"))); - assert_eq!(validate_identifier("Pizza"), None); - assert_eq!(validate_identifier("pizZa"), None); - assert!(validate_identifier("0trust").is_none()); + let s = Span::default(); + assert_eq!(validate_identifier("a", s), Some(Identifier::new("a"))); + assert_eq!(validate_identifier("ab", s), Some(Identifier::new("ab"))); + assert_eq!( + validate_identifier("johnny5", s), + Some(Identifier::new("johnny5")) + ); + assert_eq!(validate_identifier("Pizza", s), None); + assert_eq!(validate_identifier("pizZa", s), None); + assert!(validate_identifier("0trust", s).is_none()); assert_eq!( - validate_identifier("make_dinner"), - Some(Identifier("make_dinner")) + validate_identifier("make_dinner", s), + Some(Identifier::new("make_dinner")) ); - assert!(validate_identifier("MakeDinner").is_none()); - assert!(validate_identifier("make-dinner").is_none()); + assert!(validate_identifier("MakeDinner", s).is_none()); + assert!(validate_identifier("make-dinner", s).is_none()); } #[test] fn forma_rules() { - assert_eq!(validate_forma("A"), Some(Forma("A"))); - assert_eq!(validate_forma("Beans"), Some(Forma("Beans"))); - assert_eq!(validate_forma("lower"), None); + assert_eq!(validate_forma("A", Span::default()), Some(Forma::new("A"))); + assert_eq!( + validate_forma("Beans", Span::default()), + Some(Forma::new("Beans")) + ); + assert_eq!(validate_forma("lower", Span::default()), None); } #[test] fn genus_rules_single() { - assert_eq!(validate_genus("A"), Some(Genus::Single(Forma("A")))); + assert_eq!( + validate_genus("A", Span::default()), + Some(Genus::Single(Forma::new("A"))) + ); } #[test] fn genus_rules_list() { - assert_eq!(validate_genus("[A]"), Some(Genus::List(Forma("A")))); + assert_eq!( + validate_genus("[A]", Span::default()), + Some(Genus::List(Forma::new("A"))) + ); // Test list with whitespace assert_eq!( - validate_genus("[ Input ]"), - Some(Genus::List(Forma("Input"))) + validate_genus("[ Input ]", Span::default()), + Some(Genus::List(Forma::new("Input"))) ); assert_eq!( - validate_genus("[\tOutput\t]"), - Some(Genus::List(Forma("Output"))) + validate_genus("[\tOutput\t]", Span::default()), + Some(Genus::List(Forma::new("Output"))) ); // Test malformed lists - assert_eq!(validate_genus("[Input"), None); - assert_eq!(validate_genus("Input]"), None); + assert_eq!(validate_genus("[Input", Span::default()), None); + assert_eq!(validate_genus("Input]", Span::default()), None); } #[test] fn genus_rules_tuple_parens() { assert_eq!( - validate_genus("(A, B)"), - Some(Genus::Tuple(vec![Forma("A"), Forma("B")])) + validate_genus("(A, B)", Span::default()), + Some(Genus::Tuple(vec![Forma::new("A"), Forma::new("B")])) ); assert_eq!( - validate_genus("(Coffee, Tea)"), - Some(Genus::Tuple(vec![Forma("Coffee"), Forma("Tea")])) + validate_genus("(Coffee, Tea)", Span::default()), + Some(Genus::Tuple(vec![Forma::new("Coffee"), Forma::new("Tea")])) ); // not actually sure whether we should be normalizing this? Probably // not, because formatting and linting is a separate concern. - assert_eq!(validate_genus("(A)"), Some(Genus::Tuple(vec![Forma("A")]))); + assert_eq!( + validate_genus("(A)", Span::default()), + Some(Genus::Tuple(vec![Forma::new("A")])) + ); // Test parenthesized tuples with whitespace assert_eq!( - validate_genus("( A , B )"), - Some(Genus::Tuple(vec![Forma("A"), Forma("B")])) + validate_genus("( A , B )", Span::default()), + Some(Genus::Tuple(vec![Forma::new("A"), Forma::new("B")])) ); assert_eq!( - validate_genus("(\tA\t,\tB\t)"), - Some(Genus::Tuple(vec![Forma("A"), Forma("B")])) + validate_genus("(\tA\t,\tB\t)", Span::default()), + Some(Genus::Tuple(vec![Forma::new("A"), Forma::new("B")])) ); // Test malformed tuples - assert_eq!(validate_genus("(Input"), None); - assert_eq!(validate_genus("Input)"), None); + assert_eq!(validate_genus("(Input", Span::default()), None); + assert_eq!(validate_genus("Input)", Span::default()), None); } #[test] fn genus_rules_tuple_bare() { assert_eq!( - validate_genus("A, B"), - Some(Genus::Naked(vec![Forma("A"), Forma("B")])) + validate_genus("A, B", Span::default()), + Some(Genus::Naked(vec![Forma::new("A"), Forma::new("B")])) ); assert_eq!( - validate_genus("Coffee, Tea"), - Some(Genus::Naked(vec![Forma("Coffee"), Forma("Tea")])) + validate_genus("Coffee, Tea", Span::default()), + Some(Genus::Naked(vec![Forma::new("Coffee"), Forma::new("Tea")])) ); assert_eq!( - validate_genus("Input, Data, Config"), + validate_genus("Input, Data, Config", Span::default()), Some(Genus::Naked(vec![ - Forma("Input"), - Forma("Data"), - Forma("Config") + Forma::new("Input"), + Forma::new("Data"), + Forma::new("Config") ])) ); assert_eq!( - validate_genus("A,B"), - Some(Genus::Naked(vec![Forma("A"), Forma("B")])) + validate_genus("A,B", Span::default()), + Some(Genus::Naked(vec![Forma::new("A"), Forma::new("B")])) ); assert_eq!( - validate_genus("A , B"), - Some(Genus::Naked(vec![Forma("A"), Forma("B")])) + validate_genus("A , B", Span::default()), + Some(Genus::Naked(vec![Forma::new("A"), Forma::new("B")])) ); // Test edge cases with whitespace assert_eq!( - validate_genus(" A , B "), - Some(Genus::Naked(vec![Forma("A"), Forma("B")])) + validate_genus(" A , B ", Span::default()), + Some(Genus::Naked(vec![Forma::new("A"), Forma::new("B")])) ); assert_eq!( - validate_genus("\tA\t,\tB\t"), - Some(Genus::Naked(vec![Forma("A"), Forma("B")])) + validate_genus("\tA\t,\tB\t", Span::default()), + Some(Genus::Naked(vec![Forma::new("A"), Forma::new("B")])) ); } #[test] fn genus_rules_unit() { - assert_eq!(validate_genus("()"), Some(Genus::Unit)); + assert_eq!(validate_genus("()", Span::default()), Some(Genus::Unit)); // Test unit with whitespace - assert_eq!(validate_genus("( )"), Some(Genus::Unit)); - assert_eq!(validate_genus("(\t)"), Some(Genus::Unit)); + assert_eq!(validate_genus("( )", Span::default()), Some(Genus::Unit)); + assert_eq!(validate_genus("(\t)", Span::default()), Some(Genus::Unit)); } #[test] fn genus_rules_malformed() { // Test malformed brackets/parens - assert_eq!(validate_genus("[Input"), None); - assert_eq!(validate_genus("Input]"), None); - assert_eq!(validate_genus("(Input"), None); - assert_eq!(validate_genus("Input)"), None); + assert_eq!(validate_genus("[Input", Span::default()), None); + assert_eq!(validate_genus("Input]", Span::default()), None); + assert_eq!(validate_genus("(Input", Span::default()), None); + assert_eq!(validate_genus("Input)", Span::default()), None); } #[test] fn license_rules() { @@ -552,6 +818,7 @@ mod check { license: None, copyright: None, domain: None, + span: Span::default(), }; t1 @@ -564,6 +831,7 @@ mod check { license: None, copyright: None, domain: None, + span: Span::default(), }; assert_eq!(Metadata::default(), t1); @@ -573,6 +841,7 @@ mod check { license: Some("MIT"), copyright: Some("ACME, Inc"), domain: Some("checklist"), + span: Span::default(), }; let t3 = maker(); diff --git a/src/parsing/checks/errors.rs b/src/parsing/checks/errors.rs index 34fba4f2..9e882132 100644 --- a/src/parsing/checks/errors.rs +++ b/src/parsing/checks/errors.rs @@ -30,7 +30,7 @@ fn invalid_identifier_uppercase_start() { Making_Coffee : Ingredients -> Coffee "# .trim_ascii(), - ParsingError::InvalidIdentifier(0, 13, "Making_Coffee".to_string()), + ParsingError::InvalidIdentifier(Span::new(0, 13), "Making_Coffee".to_string()), ); } @@ -41,7 +41,7 @@ fn invalid_identifier_mixed_case() { makeCoffee : Ingredients -> Coffee "# .trim_ascii(), - ParsingError::InvalidIdentifier(0, 10, "makeCoffee".to_string()), + ParsingError::InvalidIdentifier(Span::new(0, 10), "makeCoffee".to_string()), ); } @@ -52,7 +52,7 @@ fn invalid_identifier_with_dashes() { make-coffee : Ingredients -> Coffee "# .trim_ascii(), - ParsingError::InvalidIdentifier(0, 11, "make-coffee".to_string()), + ParsingError::InvalidIdentifier(Span::new(0, 11), "make-coffee".to_string()), ); } @@ -63,7 +63,7 @@ fn invalid_identifier_with_spaces() { make coffee : Ingredients -> Coffee "# .trim_ascii(), - ParsingError::InvalidParameters(5, 6), + ParsingError::InvalidParameters(Span::new(5, 6)), ); } @@ -74,7 +74,7 @@ fn invalid_signature_wrong_arrow() { making_coffee : Ingredients => Coffee "# .trim_ascii(), - ParsingError::InvalidSignature(28, 0), + ParsingError::InvalidSignature(Span::new(28, 0)), ); } @@ -85,7 +85,7 @@ fn invalid_genus_lowercase_forma() { making_coffee : ingredients -> Coffee "# .trim_ascii(), - ParsingError::InvalidGenus(16, 11), + ParsingError::InvalidGenus(Span::new(16, 11)), ); } @@ -96,7 +96,7 @@ fn invalid_genus_both_lowercase() { making_coffee : ingredients -> coffee "# .trim_ascii(), - ParsingError::InvalidGenus(16, 11), + ParsingError::InvalidGenus(Span::new(16, 11)), ); } @@ -107,7 +107,7 @@ fn invalid_signature_missing_arrow() { making_coffee : Ingredients Coffee "# .trim_ascii(), - ParsingError::InvalidSignature(28, 0), + ParsingError::InvalidSignature(Span::new(28, 0)), ); } @@ -118,7 +118,7 @@ fn invalid_declaration_missing_colon() { making_coffee Ingredients -> Coffee "# .trim_ascii(), - ParsingError::Unrecognized(0, 0), + ParsingError::Unrecognized(Span::new(0, 0)), ); } @@ -129,7 +129,7 @@ fn invalid_identifier_in_parameters() { making_coffee(BadParam) : Ingredients -> Coffee "# .trim_ascii(), - ParsingError::InvalidIdentifier(0, 8, "BadParam".to_string()), + ParsingError::InvalidIdentifier(Span::new(0, 8), "BadParam".to_string()), ); } @@ -140,7 +140,7 @@ fn invalid_identifier_empty() { : Ingredients -> Coffee "# .trim_ascii(), - ParsingError::InvalidDeclaration(0, 0), + ParsingError::InvalidDeclaration(Span::new(0, 0)), ); } @@ -153,7 +153,7 @@ making_coffee : A. First step (should be lowercase 'a.') "# .trim_ascii(), - ParsingError::InvalidStep(21, 0), + ParsingError::InvalidStep(Span::new(21, 0)), ); } @@ -167,7 +167,7 @@ making_coffee : "Yes" | "No" "# .trim_ascii(), - ParsingError::InvalidResponse(52, 0), + ParsingError::InvalidResponse(Span::new(52, 0)), ); } @@ -181,7 +181,7 @@ making_coffee : This is missing closing backticks "# .trim_ascii(), - ParsingError::InvalidMultiline(24, 0), + ParsingError::InvalidMultiline(Span::new(24, 0)), ); } @@ -194,7 +194,7 @@ making_coffee : 1. Do something { exec("command" "# .trim_ascii(), - ParsingError::ExpectedMatchingChar(37, 0, "a code block", '{', '}'), + ParsingError::ExpectedMatchingChar(Span::new(37, 0), "a code block", '{', '}'), ); } @@ -207,7 +207,7 @@ making_coffee : i. Wrong case section "# .trim_ascii(), - ParsingError::InvalidStep(21, 0), + ParsingError::InvalidStep(Span::new(21, 0)), ); } @@ -220,7 +220,7 @@ making_coffee : 1. Do '), + ParsingError::ExpectedMatchingChar(Span::new(27, 0), "an invocation", '<', '>'), ); } @@ -233,7 +233,7 @@ making_coffee : 1. Do something { exec("command" } "# .trim_ascii(), - ParsingError::ExpectedMatchingChar(43, 0, "parameters for a function", '(', ')'), + ParsingError::ExpectedMatchingChar(Span::new(43, 0), "parameters for a function", '(', ')'), ); } @@ -246,7 +246,7 @@ making_coffee : 1. Do something { re peat() } "# .trim_ascii(), - ParsingError::InvalidCodeBlock(39, 10), + ParsingError::InvalidCodeBlock(Span::new(39, 10)), ); } @@ -259,7 +259,7 @@ making_coffee : 1. Do something { re peat () } "# .trim_ascii(), - ParsingError::InvalidCodeBlock(39, 18), + ParsingError::InvalidCodeBlock(Span::new(39, 18)), ); } @@ -272,7 +272,7 @@ making_coffee : 1. { repeat '), + ParsingError::ExpectedMatchingChar(Span::new(33, 0), "an invocation", '<', '>'), ); } @@ -286,7 +286,7 @@ making_coffee : A. This should be lowercase "# .trim_ascii(), - ParsingError::InvalidSubstep(43, 0), + ParsingError::InvalidSubstep(Span::new(43, 0)), ); } @@ -299,6 +299,6 @@ robot : Your plastic pal who's fun to be with! { re peat } "# .trim_ascii(), - ParsingError::InvalidCodeBlock(50, 3), + ParsingError::InvalidCodeBlock(Span::new(50, 3)), ); } diff --git a/src/parsing/checks/parser.rs b/src/parsing/checks/parser.rs index 775d8ca5..deb8f3e6 100644 --- a/src/parsing/checks/parser.rs +++ b/src/parsing/checks/parser.rs @@ -140,11 +140,11 @@ fn identifier_rules() { let mut input = Parser::new(); input.initialize("p"); let result = input.read_identifier(); - assert_eq!(result, Ok(Identifier("p"))); + assert_eq!(result, Ok(Identifier::new("p"))); input.initialize("cook_pizza"); let result = input.read_identifier(); - assert_eq!(result, Ok(Identifier("cook_pizza"))); + assert_eq!(result, Ok(Identifier::new("cook_pizza"))); input.initialize("cook-pizza"); let result = input.read_identifier(); @@ -159,8 +159,8 @@ fn signatures() { assert_eq!( result, Ok(Signature { - requires: Genus::Single(Forma("A")), - provides: Genus::Single(Forma("B")) + requires: Genus::Single(Forma::new("A")), + provides: Genus::Single(Forma::new("B")) }) ); @@ -169,8 +169,8 @@ fn signatures() { assert_eq!( result, Ok(Signature { - requires: Genus::Single(Forma("Beans")), - provides: Genus::Single(Forma("Coffee")) + requires: Genus::Single(Forma::new("Beans")), + provides: Genus::Single(Forma::new("Coffee")) }) ); @@ -179,8 +179,8 @@ fn signatures() { assert_eq!( result, Ok(Signature { - requires: Genus::List(Forma("Bits")), - provides: Genus::Single(Forma("Bob")) + requires: Genus::List(Forma::new("Bits")), + provides: Genus::Single(Forma::new("Bob")) }) ); @@ -189,8 +189,8 @@ fn signatures() { assert_eq!( result, Ok(Signature { - requires: Genus::Single(Forma("Complex")), - provides: Genus::Tuple(vec![Forma("Real"), Forma("Imaginary")]) + requires: Genus::Single(Forma::new("Complex")), + provides: Genus::Tuple(vec![Forma::new("Real"), Forma::new("Imaginary")]) }) ); } @@ -203,7 +203,7 @@ fn declaration_simple() { assert!(is_procedure_declaration(input.source)); let result = input.parse_procedure_declaration(); - assert_eq!(result, Ok((Identifier("making_coffee"), None, None))); + assert_eq!(result, Ok((Identifier::new("making_coffee"), None, None))); } #[test] @@ -216,11 +216,11 @@ fn declaration_full() { assert_eq!( result, Ok(( - Identifier("f"), + Identifier::new("f"), None, Some(Signature { - requires: Genus::Single(Forma("A")), - provides: Genus::Single(Forma("B")) + requires: Genus::Single(Forma::new("A")), + provides: Genus::Single(Forma::new("B")) }) )) ); @@ -232,11 +232,11 @@ fn declaration_full() { assert_eq!( result, Ok(( - Identifier("making_coffee"), + Identifier::new("making_coffee"), None, Some(Signature { - requires: Genus::Tuple(vec![Forma("Beans"), Forma("Milk")]), - provides: Genus::List(Forma("Coffee")) + requires: Genus::Tuple(vec![Forma::new("Beans"), Forma::new("Milk")]), + provides: Genus::List(Forma::new("Coffee")) }) )) ); @@ -289,11 +289,11 @@ making_coffee : assert_eq!( result, Ok(( - Identifier("making_coffee"), + Identifier::new("making_coffee"), None, Some(Signature { - requires: Genus::Single(Forma("Ingredients")), - provides: Genus::Single(Forma("Coffee")) + requires: Genus::Single(Forma::new("Ingredients")), + provides: Genus::Single(Forma::new("Coffee")) }) )) ); @@ -312,11 +312,11 @@ making_coffee(b, m) : assert_eq!( result, Ok(( - Identifier("making_coffee"), - Some(vec![Identifier("b"), Identifier("m")]), + Identifier::new("making_coffee"), + Some(vec![Identifier::new("b"), Identifier::new("m")]), Some(Signature { - requires: Genus::Tuple(vec![Forma("Beans"), Forma("Milk")]), - provides: Genus::Single(Forma("Coffee")) + requires: Genus::Tuple(vec![Forma::new("Beans"), Forma::new("Milk")]), + provides: Genus::Single(Forma::new("Coffee")) }) )) ); @@ -415,19 +415,19 @@ fn taking_until() { // Test take_until() with an identifier up to a limiting character input.initialize("hello,world"); let result = input.take_until(&[','], |inner| inner.read_identifier()); - assert_eq!(result, Ok(Identifier("hello"))); + assert_eq!(result, Ok(Identifier::new("hello"))); assert_eq!(input.source, ",world"); // Test take_until() with whitespace delimiters input.initialize("test \t\nmore"); let result = input.take_until(&[' ', '\t', '\n'], |inner| inner.read_identifier()); - assert_eq!(result, Ok(Identifier("test"))); + assert_eq!(result, Ok(Identifier::new("test"))); assert_eq!(input.source, " \t\nmore"); // Test take_until() when no delimiter found (it should take everything) input.initialize("onlytext"); let result = input.take_until(&[',', ';'], |inner| inner.read_identifier()); - assert_eq!(result, Ok(Identifier("onlytext"))); + assert_eq!(result, Ok(Identifier::new("onlytext"))); assert_eq!(input.source, ""); } @@ -441,7 +441,7 @@ fn reading_invocations() { assert_eq!( result, Ok(Invocation { - target: Target::Local(Identifier("hello")), + target: Target::Local(Identifier::new("hello")), parameters: None }) ); @@ -452,7 +452,7 @@ fn reading_invocations() { assert_eq!( result, Ok(Invocation { - target: Target::Local(Identifier("hello_world")), + target: Target::Local(Identifier::new("hello_world")), parameters: Some(vec![]) }) ); @@ -463,11 +463,11 @@ fn reading_invocations() { assert_eq!( result, Ok(Invocation { - target: Target::Local(Identifier("greetings")), + target: Target::Local(Identifier::new("greetings")), parameters: Some(vec![ - Expression::Variable(Identifier("name")), - Expression::Variable(Identifier("title")), - Expression::Variable(Identifier("occupation")) + Expression::Variable(Identifier::new("name"), Span::default()), + Expression::Variable(Identifier::new("title"), Span::default()), + Expression::Variable(Identifier::new("occupation"), Span::default()) ]) }) ); @@ -480,7 +480,7 @@ fn reading_invocations() { assert_eq!( result, Ok(Invocation { - target: Target::Remote(External("https://example.com/proc")), + target: Target::Remote(External::new("https://example.com/proc")), parameters: None }) ); @@ -557,8 +557,9 @@ fn read_toplevel_steps() { result, Ok(Scope::DependentBlock { ordinal: "1", - description: vec![Paragraph(vec![Descriptive::Text("First step")])], + description: vec![Paragraph::new(vec![Descriptive::Text("First step")])], subscopes: vec![], + span: Span::default(), }) ); @@ -574,10 +575,11 @@ fn read_toplevel_steps() { result, Ok(Scope::ParallelBlock { bullet: '-', - description: vec![Paragraph(vec![Descriptive::Text( + description: vec![Paragraph::new(vec![Descriptive::Text( "a top-level task to be one in parallel with" )]),], subscopes: vec![], + span: Span::default(), }) ); let result = input.read_step_parallel(); @@ -585,8 +587,11 @@ fn read_toplevel_steps() { result, Ok(Scope::ParallelBlock { bullet: '-', - description: vec![Paragraph(vec![Descriptive::Text("another top-level task")]),], + description: vec![Paragraph::new(vec![Descriptive::Text( + "another top-level task" + )]),], subscopes: vec![], + span: Span::default(), }) ); @@ -601,17 +606,18 @@ fn read_toplevel_steps() { result, Ok(Scope::DependentBlock { ordinal: "1", - description: vec![Paragraph(vec![Descriptive::Text( + description: vec![Paragraph::new(vec![Descriptive::Text( "Have you done the first thing in the first one?" )])], subscopes: vec![], + span: Span::default(), }) ); // Test invalid step input.initialize("Not a step"); let result = input.read_step_dependent(); - assert_eq!(result, Err(ParsingError::InvalidStep(0, 0))); + assert_eq!(result, Err(ParsingError::InvalidStep(Span::new(0, 0)))); } #[test] @@ -625,8 +631,11 @@ fn reading_substeps_basic() { result, Ok(Scope::DependentBlock { ordinal: "a", - description: vec![Paragraph(vec![Descriptive::Text("First subordinate task")])], + description: vec![Paragraph::new(vec![Descriptive::Text( + "First subordinate task" + )])], subscopes: vec![], + span: Span::default(), }) ); @@ -637,8 +646,9 @@ fn reading_substeps_basic() { result, Ok(Scope::ParallelBlock { bullet: '-', - description: vec![Paragraph(vec![Descriptive::Text("Parallel task")])], + description: vec![Paragraph::new(vec![Descriptive::Text("Parallel task")])], subscopes: vec![], + span: Span::default(), }) ); } @@ -660,19 +670,22 @@ fn single_step_with_dependent_substeps() { result, Ok(Scope::DependentBlock { ordinal: "1", - description: vec![Paragraph(vec![Descriptive::Text("Main step")])], + description: vec![Paragraph::new(vec![Descriptive::Text("Main step")])], subscopes: vec![ Scope::DependentBlock { ordinal: "a", - description: vec![Paragraph(vec![Descriptive::Text("First substep")])], + description: vec![Paragraph::new(vec![Descriptive::Text("First substep")])], subscopes: vec![], + span: Span::default(), }, Scope::DependentBlock { ordinal: "b", - description: vec![Paragraph(vec![Descriptive::Text("Second substep")])], + description: vec![Paragraph::new(vec![Descriptive::Text("Second substep")])], subscopes: vec![], + span: Span::default(), }, ], + span: Span::default(), }) ); } @@ -694,19 +707,22 @@ fn single_step_with_parallel_substeps() { result, Ok(Scope::DependentBlock { ordinal: "1", - description: vec![Paragraph(vec![Descriptive::Text("Main step")])], + description: vec![Paragraph::new(vec![Descriptive::Text("Main step")])], subscopes: vec![ Scope::ParallelBlock { bullet: '-', - description: vec![Paragraph(vec![Descriptive::Text("First substep")])], + description: vec![Paragraph::new(vec![Descriptive::Text("First substep")])], subscopes: vec![], + span: Span::default(), }, Scope::ParallelBlock { bullet: '-', - description: vec![Paragraph(vec![Descriptive::Text("Second substep")])], + description: vec![Paragraph::new(vec![Descriptive::Text("Second substep")])], subscopes: vec![], + span: Span::default(), }, ], + span: Span::default(), }) ); } @@ -729,12 +745,14 @@ fn multiple_steps_with_substeps() { first_result, Ok(Scope::DependentBlock { ordinal: "1", - description: vec![Paragraph(vec![Descriptive::Text("First step")])], + description: vec![Paragraph::new(vec![Descriptive::Text("First step")])], subscopes: vec![Scope::DependentBlock { ordinal: "a", - description: vec![Paragraph(vec![Descriptive::Text("Substep")])], + description: vec![Paragraph::new(vec![Descriptive::Text("Substep")])], subscopes: vec![], + span: Span::default(), }], + span: Span::default(), }) ); @@ -742,8 +760,9 @@ fn multiple_steps_with_substeps() { second_result, Ok(Scope::DependentBlock { ordinal: "2", - description: vec![Paragraph(vec![Descriptive::Text("Second step")])], + description: vec![Paragraph::new(vec![Descriptive::Text("Second step")])], subscopes: vec![], + span: Span::default(), }) ); } @@ -792,27 +811,32 @@ fn read_step_with_content() { result, Ok(Scope::DependentBlock { ordinal: "1", - description: vec![Paragraph(vec![Descriptive::Text( + description: vec![Paragraph::new(vec![Descriptive::Text( "Have you done the first thing in the first one?" )])], subscopes: vec![Scope::DependentBlock { ordinal: "a", - description: vec![Paragraph(vec![Descriptive::Text( + description: vec![Paragraph::new(vec![Descriptive::Text( "Do the first thing. Then ask yourself if you are done:" )])], subscopes: vec![Scope::ResponseBlock { responses: vec![ Response { value: "Yes", - condition: None + condition: None, + span: Span::default() }, Response { value: "No", - condition: Some("but I have an excuse") + condition: Some("but I have an excuse"), + span: Span::default() } - ] - }] + ], + span: Span::default(), + }], + span: Span::default(), }], + span: Span::default(), }) ); @@ -850,7 +874,7 @@ This is the first one. .elements .iter() .find_map(|element| match element { - Element::Steps(steps) => Some(steps), + Element::Steps(steps, _) => Some(steps), _ => None, }); assert_eq!( @@ -1090,17 +1114,29 @@ fn code_blocks() { // Test simple identifier in code block input.initialize("{ count }"); let result = input.read_code_block(); - assert_eq!(result, Ok(vec![Expression::Variable(Identifier("count"))])); + assert_eq!( + result, + Ok(vec![Expression::Variable( + Identifier::new("count"), + Span::default() + )]) + ); // Test function with simple parameter input.initialize("{ sum(count) }"); let result = input.read_code_block(); assert_eq!( result, - Ok(vec![Expression::Execution(Function { - target: Identifier("sum"), - parameters: vec![Expression::Variable(Identifier("count"))] - })]) + Ok(vec![Expression::Execution( + Function { + target: Identifier::new("sum"), + parameters: vec![Expression::Variable( + Identifier::new("count"), + Span::default() + )] + }, + Span::default() + )]) ); // Test function with multiple parameters @@ -1108,14 +1144,17 @@ fn code_blocks() { let result = input.read_code_block(); assert_eq!( result, - Ok(vec![Expression::Execution(Function { - target: Identifier("consume"), - parameters: vec![ - Expression::Variable(Identifier("apple")), - Expression::Variable(Identifier("banana")), - Expression::Variable(Identifier("chocolate")) - ] - })]) + Ok(vec![Expression::Execution( + Function { + target: Identifier::new("consume"), + parameters: vec![ + Expression::Variable(Identifier::new("apple"), Span::default()), + Expression::Variable(Identifier::new("banana"), Span::default()), + Expression::Variable(Identifier::new("chocolate"), Span::default()) + ] + }, + Span::default() + )]) ); // Test function with text parameter @@ -1123,10 +1162,16 @@ fn code_blocks() { let result = input.read_code_block(); assert_eq!( result, - Ok(vec![Expression::Execution(Function { - target: Identifier("exec"), - parameters: vec![Expression::String(vec![Piece::Text("Hello, World")])] - })]) + Ok(vec![Expression::Execution( + Function { + target: Identifier::new("exec"), + parameters: vec![Expression::String( + vec![Piece::Text("Hello, World")], + Span::default() + )] + }, + Span::default() + )]) ); // Test function with multiline string parameter @@ -1138,13 +1183,17 @@ echo "Done"```) }"#, let result = input.read_code_block(); assert_eq!( result, - Ok(vec![Expression::Execution(Function { - target: Identifier("exec"), - parameters: vec![Expression::Multiline( - Some("bash"), - vec!["ls -l", "echo \"Done\""] - )] - })]) + Ok(vec![Expression::Execution( + Function { + target: Identifier::new("exec"), + parameters: vec![Expression::Multiline( + Some("bash"), + vec!["ls -l", "echo \"Done\""], + Span::default() + )] + }, + Span::default() + )]) ); // Test function with quantity parameter (like timer with duration) @@ -1152,18 +1201,24 @@ echo "Done"```) }"#, let result = input.read_code_block(); assert_eq!( result, - Ok(vec![Expression::Execution(Function { - target: Identifier("timer"), - parameters: vec![Expression::Number(Numeric::Scientific(Quantity { - mantissa: Decimal { - number: 3, - precision: 0 - }, - uncertainty: None, - magnitude: None, - symbol: "hr" - }))] - })]) + Ok(vec![Expression::Execution( + Function { + target: Identifier::new("timer"), + parameters: vec![Expression::Number( + Numeric::Scientific(Quantity { + mantissa: Decimal { + number: 3, + precision: 0 + }, + uncertainty: None, + magnitude: None, + symbol: "hr" + }), + Span::default() + )] + }, + Span::default() + )]) ); // Test function with integer quantity parameter @@ -1171,10 +1226,13 @@ echo "Done"```) }"#, let result = input.read_code_block(); assert_eq!( result, - Ok(vec![Expression::Execution(Function { - target: Identifier("measure"), - parameters: vec![Expression::Number(Numeric::Integral(100))] - })]) + Ok(vec![Expression::Execution( + Function { + target: Identifier::new("measure"), + parameters: vec![Expression::Number(Numeric::Integral(100), Span::default())] + }, + Span::default() + )]) ); // Test function with multiple integer parameters @@ -1182,13 +1240,16 @@ echo "Done"```) }"#, let result = input.read_code_block(); assert_eq!( result, - Ok(vec![Expression::Execution(Function { - target: Identifier("seq"), - parameters: vec![ - Expression::Number(Numeric::Integral(1)), - Expression::Number(Numeric::Integral(6)) - ] - })]) + Ok(vec![Expression::Execution( + Function { + target: Identifier::new("seq"), + parameters: vec![ + Expression::Number(Numeric::Integral(1), Span::default()), + Expression::Number(Numeric::Integral(6), Span::default()) + ] + }, + Span::default() + )]) ); // Test function with decimal quantity parameter @@ -1196,21 +1257,27 @@ echo "Done"```) }"#, let result = input.read_code_block(); assert_eq!( result, - Ok(vec![Expression::Execution(Function { - target: Identifier("wait"), - parameters: vec![ - Expression::Number(Numeric::Scientific(Quantity { - mantissa: Decimal { - number: 25, - precision: 1 - }, - uncertainty: None, - magnitude: None, - symbol: "s" - })), - Expression::String(vec![Piece::Text("yes")]) - ] - })]) + Ok(vec![Expression::Execution( + Function { + target: Identifier::new("wait"), + parameters: vec![ + Expression::Number( + Numeric::Scientific(Quantity { + mantissa: Decimal { + number: 25, + precision: 1 + }, + uncertainty: None, + magnitude: None, + symbol: "s" + }), + Span::default() + ), + Expression::String(vec![Piece::Text("yes")], Span::default()) + ] + }, + Span::default() + )]) ); } @@ -1231,20 +1298,24 @@ fn multiline() { let result = input.read_code_block(); assert_eq!( result, - Ok(vec![Expression::Execution(Function { - target: Identifier("exec"), - parameters: vec![Expression::Multiline( - Some("bash"), - vec![ - "./stuff", - "", - "if [ true ]", - "then", - " ./other args", - "fi" - ] - )] - })]) + Ok(vec![Expression::Execution( + Function { + target: Identifier::new("exec"), + parameters: vec![Expression::Multiline( + Some("bash"), + vec![ + "./stuff", + "", + "if [ true ]", + "then", + " ./other args", + "fi" + ], + Span::default() + )] + }, + Span::default() + )]) ); // Test multiline without language tag @@ -1256,10 +1327,17 @@ echo "Done"```) }"#, let result = input.read_code_block(); assert_eq!( result, - Ok(vec![Expression::Execution(Function { - target: Identifier("exec"), - parameters: vec![Expression::Multiline(None, vec!["ls -l", "echo \"Done\""])] - })]) + Ok(vec![Expression::Execution( + Function { + target: Identifier::new("exec"), + parameters: vec![Expression::Multiline( + None, + vec!["ls -l", "echo \"Done\""], + Span::default() + )] + }, + Span::default() + )]) ); // Test multiline with intentional empty lines in the middle @@ -1275,20 +1353,24 @@ echo "Ending"```) }"#, let result = input.read_code_block(); assert_eq!( result, - Ok(vec![Expression::Execution(Function { - target: Identifier("exec"), - parameters: vec![Expression::Multiline( - Some("shell"), - vec![ - "echo \"Starting\"", - "", - "echo \"Middle section\"", - "", - "", - "echo \"Ending\"" - ] - )] - })]) + Ok(vec![Expression::Execution( + Function { + target: Identifier::new("exec"), + parameters: vec![Expression::Multiline( + Some("shell"), + vec![ + "echo \"Starting\"", + "", + "echo \"Middle section\"", + "", + "", + "echo \"Ending\"" + ], + Span::default() + )] + }, + Span::default() + )]) ); // Test that internal indentation relative to the base is preserved, @@ -1306,20 +1388,24 @@ echo "Ending"```) }"#, let result = input.read_code_block(); assert_eq!( result, - Ok(vec![Expression::Execution(Function { - target: Identifier("exec"), - parameters: vec![Expression::Multiline( - Some("python"), - vec![ - "def hello():", - " print(\"Hello\")", - " if True:", - " print(\"World\")", - "", - "hello()" - ] - )] - })]) + Ok(vec![Expression::Execution( + Function { + target: Identifier::new("exec"), + parameters: vec![Expression::Multiline( + Some("python"), + vec![ + "def hello():", + " print(\"Hello\")", + " if True:", + " print(\"World\")", + "", + "hello()" + ], + Span::default() + )] + }, + Span::default() + )]) ); // Test that a trailing empty line from the closing delimiter is removed @@ -1331,10 +1417,17 @@ echo test let result = input.read_code_block(); assert_eq!( result, - Ok(vec![Expression::Execution(Function { - target: Identifier("exec"), - parameters: vec![Expression::Multiline(None, vec!["echo test"])] - })]) + Ok(vec![Expression::Execution( + Function { + target: Identifier::new("exec"), + parameters: vec![Expression::Multiline( + None, + vec!["echo test"], + Span::default() + )] + }, + Span::default() + )]) ); // Test various indentation edge cases @@ -1350,20 +1443,24 @@ echo test let result = input.read_code_block(); assert_eq!( result, - Ok(vec![Expression::Execution(Function { - target: Identifier("exec"), - parameters: vec![Expression::Multiline( - Some("yaml"), - vec![ - "name: test", - "items:", - " - item1", - " - item2", - "config:", - " enabled: true" - ] - )] - })]) + Ok(vec![Expression::Execution( + Function { + target: Identifier::new("exec"), + parameters: vec![Expression::Multiline( + Some("yaml"), + vec![ + "name: test", + "items:", + " - item1", + " - item2", + "config:", + " enabled: true" + ], + Span::default() + )] + }, + Span::default() + )]) ); } @@ -1376,10 +1473,13 @@ fn tablets() { let result = input.read_code_block(); assert_eq!( result, - Ok(vec![Expression::Tablet(vec![Pair { - label: "name", - value: Expression::String(vec![Piece::Text("Johannes Grammerly")]) - }])]) + Ok(vec![Expression::Tablet( + vec![Pair { + label: "name", + value: Expression::String(vec![Piece::Text("Johannes Grammerly")], Span::default()) + }], + Span::default() + )]) ); // Test multiline tablet with string values @@ -1392,16 +1492,22 @@ fn tablets() { let result = input.read_code_block(); assert_eq!( result, - Ok(vec![Expression::Tablet(vec![ - Pair { - label: "name", - value: Expression::String(vec![Piece::Text("Alice of Chains")]) - }, - Pair { - label: "age", - value: Expression::String(vec![Piece::Text("29")]) - } - ])]) + Ok(vec![Expression::Tablet( + vec![ + Pair { + label: "name", + value: Expression::String( + vec![Piece::Text("Alice of Chains")], + Span::default() + ) + }, + Pair { + label: "age", + value: Expression::String(vec![Piece::Text("29")], Span::default()) + } + ], + Span::default() + )]) ); // Test tablet with mixed value types @@ -1415,29 +1521,38 @@ fn tablets() { let result = input.read_code_block(); assert_eq!( result, - Ok(vec![Expression::Tablet(vec![ - Pair { - label: "answer", - value: Expression::Number(Numeric::Integral(42)) - }, - Pair { - label: "message", - value: Expression::Variable(Identifier("msg")) - }, - Pair { - label: "timestamp", - value: Expression::Execution(Function { - target: Identifier("now"), - parameters: vec![] - }) - } - ])]) + Ok(vec![Expression::Tablet( + vec![ + Pair { + label: "answer", + value: Expression::Number(Numeric::Integral(42), Span::default()) + }, + Pair { + label: "message", + value: Expression::Variable(Identifier::new("msg"), Span::default()) + }, + Pair { + label: "timestamp", + value: Expression::Execution( + Function { + target: Identifier::new("now"), + parameters: vec![] + }, + Span::default() + ) + } + ], + Span::default() + )]) ); // Test empty tablet input.initialize("{ [ ] }"); let result = input.read_code_block(); - assert_eq!(result, Ok(vec![Expression::Tablet(vec![])])); + assert_eq!( + result, + Ok(vec![Expression::Tablet(vec![], Span::default())]) + ); // Test tablet with interpolated string values input.initialize( @@ -1449,16 +1564,22 @@ fn tablets() { let result = input.read_code_block(); assert_eq!( result, - Ok(vec![Expression::Tablet(vec![ - Pair { - label: "context", - value: Expression::String(vec![Piece::Text("Details about the thing")]) - }, - Pair { - label: "status", - value: Expression::Variable(Identifier("active")) - } - ])]) + Ok(vec![Expression::Tablet( + vec![ + Pair { + label: "context", + value: Expression::String( + vec![Piece::Text("Details about the thing")], + Span::default() + ) + }, + Pair { + label: "status", + value: Expression::Variable(Identifier::new("active"), Span::default()) + } + ], + Span::default() + )]) ); } @@ -1469,20 +1590,35 @@ fn numeric_literals() { // Test simple integer input.initialize("{ 42 }"); let result = input.read_code_block(); - assert_eq!(result, Ok(vec![Expression::Number(Numeric::Integral(42))])); + assert_eq!( + result, + Ok(vec![Expression::Number( + Numeric::Integral(42), + Span::default() + )]) + ); // Test negative integer input.initialize("{ -123 }"); let result = input.read_code_block(); assert_eq!( result, - Ok(vec![Expression::Number(Numeric::Integral(-123))]) + Ok(vec![Expression::Number( + Numeric::Integral(-123), + Span::default() + )]) ); // Test zero input.initialize("{ 0 }"); let result = input.read_code_block(); - assert_eq!(result, Ok(vec![Expression::Number(Numeric::Integral(0))])); + assert_eq!( + result, + Ok(vec![Expression::Number( + Numeric::Integral(0), + Span::default() + )]) + ); } #[test] @@ -1492,25 +1628,25 @@ fn reading_identifiers() { // Parse a basic identifier input.initialize("hello"); let result = input.read_identifier(); - assert_eq!(result, Ok(Identifier("hello"))); + assert_eq!(result, Ok(Identifier::new("hello"))); assert_eq!(input.source, ""); // Parse an identifier with trailing content input.initialize("count more"); let result = input.read_identifier(); - assert_eq!(result, Ok(Identifier("count"))); + assert_eq!(result, Ok(Identifier::new("count"))); assert_eq!(input.source, " more"); // Parse an identifier with leading whitespace and trailing content input.initialize(" \t test_name after"); let result = input.read_identifier(); - assert_eq!(result, Ok(Identifier("test_name"))); + assert_eq!(result, Ok(Identifier::new("test_name"))); assert_eq!(input.source, " after"); // Parse an identifier with various delimiters input.initialize("name(param)"); let result = input.read_identifier(); - assert_eq!(result, Ok(Identifier("name"))); + assert_eq!(result, Ok(Identifier::new("name"))); assert_eq!(input.source, "(param)"); } @@ -1523,8 +1659,12 @@ fn test_foreach_expression() { assert_eq!( result, Ok(vec![Expression::Foreach( - vec![Identifier("item")], - Box::new(Expression::Variable(Identifier("items"))) + vec![Identifier::new("item")], + Box::new(Expression::Variable( + Identifier::new("items"), + Span::default() + )), + Span::default() )]) ); } @@ -1538,14 +1678,18 @@ fn foreach_tuple_pattern() { assert_eq!( result, Ok(vec![Expression::Foreach( - vec![Identifier("design"), Identifier("component")], - Box::new(Expression::Execution(Function { - target: Identifier("zip"), - parameters: vec![ - Expression::Variable(Identifier("designs")), - Expression::Variable(Identifier("components")) - ] - })) + vec![Identifier::new("design"), Identifier::new("component")], + Box::new(Expression::Execution( + Function { + target: Identifier::new("zip"), + parameters: vec![ + Expression::Variable(Identifier::new("designs"), Span::default()), + Expression::Variable(Identifier::new("components"), Span::default()) + ] + }, + Span::default() + )), + Span::default() )]) ); @@ -1555,15 +1699,23 @@ fn foreach_tuple_pattern() { assert_eq!( result, Ok(vec![Expression::Foreach( - vec![Identifier("a"), Identifier("b"), Identifier("c")], - Box::new(Expression::Execution(Function { - target: Identifier("zip"), - parameters: vec![ - Expression::Variable(Identifier("list1")), - Expression::Variable(Identifier("list2")), - Expression::Variable(Identifier("list3")) - ] - })) + vec![ + Identifier::new("a"), + Identifier::new("b"), + Identifier::new("c") + ], + Box::new(Expression::Execution( + Function { + target: Identifier::new("zip"), + parameters: vec![ + Expression::Variable(Identifier::new("list1"), Span::default()), + Expression::Variable(Identifier::new("list2"), Span::default()), + Expression::Variable(Identifier::new("list3"), Span::default()) + ] + }, + Span::default() + )), + Span::default() )]) ); } @@ -1577,11 +1729,15 @@ fn tuple_binding_expression() { assert_eq!( result, Ok(vec![Expression::Binding( - Box::new(Expression::Application(Invocation { - target: Target::Local(Identifier("get_coordinates")), - parameters: Some(vec![]) - })), - vec![Identifier("x"), Identifier("y")] + Box::new(Expression::Application( + Invocation { + target: Target::Local(Identifier::new("get_coordinates")), + parameters: Some(vec![]) + }, + Span::default() + )), + vec![Identifier::new("x"), Identifier::new("y")], + Span::default() )]) ); } @@ -1594,9 +1750,13 @@ fn test_repeat_expression() { let result = input.read_code_block(); assert_eq!( result, - Ok(vec![Expression::Repeat(Box::new(Expression::Variable( - Identifier("count") - )))]) + Ok(vec![Expression::Repeat( + Box::new(Expression::Variable( + Identifier::new("count"), + Span::default() + )), + Span::default() + )]) ); } @@ -1607,7 +1767,10 @@ fn test_foreach_keyword_boundary() { input.initialize("{ foreachitem in items }"); let result = input.read_code_block(); - assert_eq!(result, Err(ParsingError::InvalidCodeBlock(2, 12))); + assert_eq!( + result, + Err(ParsingError::InvalidCodeBlock(Span::new(2, 12))) + ); } #[test] @@ -1620,7 +1783,10 @@ fn test_repeat_keyword_boundary() { // Should parse as identifier, not repeat assert_eq!( result, - Ok(vec![Expression::Variable(Identifier("repeater"))]) + Ok(vec![Expression::Variable( + Identifier::new("repeater"), + Span::default() + )]) ); } @@ -1645,9 +1811,9 @@ fn splitting_by() { assert_eq!( result, Ok(vec![ - Identifier("apple"), - Identifier("banana"), - Identifier("cherry") + Identifier::new("apple"), + Identifier::new("banana"), + Identifier::new("cherry") ]) ); assert_eq!(input.source, ""); @@ -1658,16 +1824,16 @@ fn splitting_by() { assert_eq!( result, Ok(vec![ - Identifier("un"), - Identifier("deux"), - Identifier("trois") + Identifier::new("un"), + Identifier::new("deux"), + Identifier::new("trois") ]) ); // Ensure a single item (no delimiter present in input) works input.initialize("seulement"); let result = input.take_split_by(',', |inner| inner.read_identifier()); - assert_eq!(result, Ok(vec![Identifier("seulement")])); + assert_eq!(result, Ok(vec![Identifier::new("seulement")])); // an empty chunk causes an error input.initialize("un,,trois"); @@ -1682,22 +1848,26 @@ fn splitting_by() { // different split character input.initialize("'Yes'|'No'|'Maybe'"); let result = input.take_split_by('|', |inner| { - validate_response(inner.source).ok_or(ParsingError::IllegalParserState(inner.offset, 0)) + validate_response(inner.source) + .ok_or(ParsingError::IllegalParserState(Span::new(inner.offset, 0))) }); assert_eq!( result, Ok(vec![ Response { value: "Yes", - condition: None + condition: None, + span: Span::default() }, Response { value: "No", - condition: None + condition: None, + span: Span::default() }, Response { value: "Maybe", - condition: None + condition: None, + span: Span::default() } ]) ); @@ -1714,7 +1884,8 @@ fn reading_responses() { result, Ok(vec![Response { value: "Yes", - condition: None + condition: None, + span: Span::default() }]) ); @@ -1726,11 +1897,13 @@ fn reading_responses() { Ok(vec![ Response { value: "Yes", - condition: None + condition: None, + span: Span::default() }, Response { value: "No", - condition: None + condition: None, + span: Span::default() } ]) ); @@ -1743,15 +1916,18 @@ fn reading_responses() { Ok(vec![ Response { value: "Yes", - condition: None + condition: None, + span: Span::default() }, Response { value: "No", - condition: None + condition: None, + span: Span::default() }, Response { value: "Not Applicable", - condition: None + condition: None, + span: Span::default() } ]) ); @@ -1763,7 +1939,8 @@ fn reading_responses() { result, Ok(vec![Response { value: "Yes", - condition: Some("and equipment available") + condition: Some("and equipment available"), + span: Span::default() }]) ); @@ -1775,11 +1952,13 @@ fn reading_responses() { Ok(vec![ Response { value: "Option A", - condition: None + condition: None, + span: Span::default() }, Response { value: "Option B", - condition: None + condition: None, + span: Span::default() } ]) ); @@ -1792,12 +1971,24 @@ fn reading_attributes() { // Test simple role input.initialize("@chef"); let result = input.read_attributes(); - assert_eq!(result, Ok(vec![Attribute::Role(Identifier("chef"))])); + assert_eq!( + result, + Ok(vec![Attribute::Role( + Identifier::new("chef"), + Span::default() + )]) + ); // Test simple place input.initialize("^kitchen"); let result = input.read_attributes(); - assert_eq!(result, Ok(vec![Attribute::Place(Identifier("kitchen"))])); + assert_eq!( + result, + Ok(vec![Attribute::Place( + Identifier::new("kitchen"), + Span::default() + )]) + ); // Test multiple roles input.initialize("@master_chef + @barista"); @@ -1805,8 +1996,8 @@ fn reading_attributes() { assert_eq!( result, Ok(vec![ - Attribute::Role(Identifier("master_chef")), - Attribute::Role(Identifier("barista")) + Attribute::Role(Identifier::new("master_chef"), Span::default()), + Attribute::Role(Identifier::new("barista"), Span::default()) ]) ); @@ -1816,8 +2007,8 @@ fn reading_attributes() { assert_eq!( result, Ok(vec![ - Attribute::Place(Identifier("kitchen")), - Attribute::Place(Identifier("bath_room")) + Attribute::Place(Identifier::new("kitchen"), Span::default()), + Attribute::Place(Identifier::new("bath_room"), Span::default()) ]) ); @@ -1827,8 +2018,8 @@ fn reading_attributes() { assert_eq!( result, Ok(vec![ - Attribute::Role(Identifier("chef")), - Attribute::Place(Identifier("bathroom")) + Attribute::Role(Identifier::new("chef"), Span::default()), + Attribute::Place(Identifier::new("bathroom"), Span::default()) ]) ); @@ -1838,8 +2029,8 @@ fn reading_attributes() { assert_eq!( result, Ok(vec![ - Attribute::Place(Identifier("kitchen")), - Attribute::Role(Identifier("barista")) + Attribute::Place(Identifier::new("kitchen"), Span::default()), + Attribute::Role(Identifier::new("barista"), Span::default()) ]) ); @@ -1849,10 +2040,10 @@ fn reading_attributes() { assert_eq!( result, Ok(vec![ - Attribute::Role(Identifier("chef")), - Attribute::Place(Identifier("kitchen")), - Attribute::Role(Identifier("barista")), - Attribute::Place(Identifier("dining_room")) + Attribute::Role(Identifier::new("chef"), Span::default()), + Attribute::Place(Identifier::new("kitchen"), Span::default()), + Attribute::Role(Identifier::new("barista"), Span::default()), + Attribute::Place(Identifier::new("dining_room"), Span::default()) ]) ); @@ -1867,6 +2058,46 @@ fn reading_attributes() { assert!(result.is_err()); } +#[test] +fn attribute_spans_include_marker() { + let mut input = Parser::new(); + + input.initialize("@chef"); + let result = input + .read_attributes() + .unwrap(); + assert_eq!( + result[0], + Attribute::Role(Identifier::new("chef"), Span::default()) + ); + if let Attribute::Role(id, span) = &result[0] { + assert_eq!(*span, Span::new(0, 5)); + assert_eq!(id.span, Span::new(1, 4)); + } + + input.initialize("^kitchen"); + let result = input + .read_attributes() + .unwrap(); + if let Attribute::Place(id, span) = &result[0] { + assert_eq!(*span, Span::new(0, 8)); + assert_eq!(id.span, Span::new(1, 7)); + } + + input.initialize("@waiter + ^milliways"); + let result = input + .read_attributes() + .unwrap(); + if let Attribute::Role(id, span) = &result[0] { + assert_eq!(*span, Span::new(0, 7)); + assert_eq!(id.span, Span::new(1, 6)); + } + if let Attribute::Place(id, span) = &result[1] { + assert_eq!(*span, Span::new(10, 10)); + assert_eq!(id.span, Span::new(11, 9)); + } +} + #[test] fn step_with_role_assignment() { let mut input = Parser::new(); @@ -1887,13 +2118,15 @@ fn step_with_role_assignment() { Scope::DependentBlock { ordinal: "1", - description: vec![Paragraph(vec![Descriptive::Text( + description: vec![Paragraph::new(vec![Descriptive::Text( "Check the patient's vital signs" )])], subscopes: vec![Scope::AttributeBlock { - attributes: vec![Attribute::Role(Identifier("nurse"))], + attributes: vec![Attribute::Role(Identifier::new("nurse"), Span::default())], subscopes: vec![], - }] + span: Span::default(), + }], + span: Span::default(), } ); } @@ -1918,17 +2151,20 @@ fn substep_with_role_assignment() { scope, Scope::DependentBlock { ordinal: "1", - description: vec![Paragraph(vec![Descriptive::Text( + description: vec![Paragraph::new(vec![Descriptive::Text( "Verify patient identity" )])], subscopes: vec![Scope::AttributeBlock { - attributes: vec![Attribute::Role(Identifier("surgeon"))], + attributes: vec![Attribute::Role(Identifier::new("surgeon"), Span::default())], subscopes: vec![Scope::DependentBlock { ordinal: "a", - description: vec![Paragraph(vec![Descriptive::Text("Check ID")])], - subscopes: vec![] - }] - }] + description: vec![Paragraph::new(vec![Descriptive::Text("Check ID")])], + subscopes: vec![], + span: Span::default(), + }], + span: Span::default(), + }], + span: Span::default(), } ); } @@ -1953,15 +2189,23 @@ fn parallel_step_with_role_assignment() { scope, Scope::DependentBlock { ordinal: "1", - description: vec![Paragraph(vec![Descriptive::Text("Monitor patient vitals")])], + description: vec![Paragraph::new(vec![Descriptive::Text( + "Monitor patient vitals" + )])], subscopes: vec![Scope::AttributeBlock { - attributes: vec![Attribute::Role(Identifier("nursing_team"))], + attributes: vec![Attribute::Role( + Identifier::new("nursing_team"), + Span::default() + )], subscopes: vec![Scope::ParallelBlock { bullet: '-', - description: vec![Paragraph(vec![Descriptive::Text("Check readings")])], - subscopes: vec![] - }] - }] + description: vec![Paragraph::new(vec![Descriptive::Text("Check readings")])], + subscopes: vec![], + span: Span::default(), + }], + span: Span::default(), + }], + span: Span::default(), } ); } @@ -1989,29 +2233,34 @@ fn two_roles_with_substeps() { scope, Scope::DependentBlock { ordinal: "1", - description: vec![Paragraph(vec![Descriptive::Text("Review events.")])], + description: vec![Paragraph::new(vec![Descriptive::Text("Review events.")])], subscopes: vec![ Scope::AttributeBlock { - attributes: vec![Attribute::Role(Identifier("surgeon"))], + attributes: vec![Attribute::Role(Identifier::new("surgeon"), Span::default())], subscopes: vec![Scope::DependentBlock { ordinal: "a", - description: vec![Paragraph(vec![Descriptive::Text( + description: vec![Paragraph::new(vec![Descriptive::Text( "What are the steps?" )])], - subscopes: vec![] - }] + subscopes: vec![], + span: Span::default(), + }], + span: Span::default(), }, Scope::AttributeBlock { - attributes: vec![Attribute::Role(Identifier("nurse"))], + attributes: vec![Attribute::Role(Identifier::new("nurse"), Span::default())], subscopes: vec![Scope::DependentBlock { ordinal: "b", - description: vec![Paragraph(vec![Descriptive::Text( + description: vec![Paragraph::new(vec![Descriptive::Text( "What are the concerns?" )])], - subscopes: vec![] - }] + subscopes: vec![], + span: Span::default(), + }], + span: Span::default(), } - ] + ], + span: Span::default(), } ); } @@ -2044,7 +2293,7 @@ fn parse_collecting_errors_basic() { assert!(errors.len() > 0); assert!(errors .iter() - .any(|e| matches!(e, ParsingError::InvalidHeader(_, _)))); + .any(|e| matches!(e, ParsingError::InvalidHeader(_)))); } } @@ -2141,7 +2390,7 @@ fn test_redundant_error_removal_unclosed_interpolation() { // Should get the specific UnclosedInterpolation error, not a generic // one match result { - Err(ParsingError::UnclosedInterpolation(_, _)) => { + Err(ParsingError::UnclosedInterpolation(_)) => { // Good - we got the specific error } Err(other) => { @@ -2190,10 +2439,10 @@ This is { exec(a, // Second element should be the multiline code inline match &descriptives[1] { - Descriptive::CodeInline(Expression::Execution(func)) => { + Descriptive::CodeInline(Expression::Execution(func, _)) => { assert_eq!( func.target - .0, + .value, "exec" ); assert_eq!( @@ -2202,7 +2451,7 @@ This is { exec(a, 3 ); // Check that all parameters were parsed correctly - if let Expression::Variable(Identifier(name)) = &func.parameters[0] { + if let Expression::Variable(Identifier { value: name, .. }, _) = &func.parameters[0] { assert_eq!(*name, "a"); } else { panic!("First parameter should be variable 'a'"); diff --git a/src/parsing/checks/verify.rs b/src/parsing/checks/verify.rs index 2e1e5ec1..e6fef53a 100644 --- a/src/parsing/checks/verify.rs +++ b/src/parsing/checks/verify.rs @@ -20,7 +20,8 @@ fn technique_header() { version: 1, license: None, copyright: None, - domain: None + domain: None, + span: Span::default(), }) ); @@ -39,7 +40,8 @@ fn technique_header() { version: 1, license: Some("MIT"), copyright: Some("ACME, Inc"), - domain: Some("checklist") + domain: Some("checklist"), + span: Span::default(), }) ); } @@ -58,13 +60,14 @@ making_coffee : (Beans, Milk) -> Coffee assert_eq!( procedure, Ok(Procedure { - name: Identifier("making_coffee"), + name: Identifier::new("making_coffee"), parameters: None, signature: Some(Signature { - requires: Genus::Tuple(vec![Forma("Beans"), Forma("Milk")]), - provides: Genus::Single(Forma("Coffee")) + requires: Genus::Tuple(vec![Forma::new("Beans"), Forma::new("Milk")]), + provides: Genus::Single(Forma::new("Coffee")) }), elements: vec![], + span: Span::default(), }) ); } @@ -85,13 +88,14 @@ second : C -> D assert_eq!( procedure, Ok(Procedure { - name: Identifier("first"), + name: Identifier::new("first"), parameters: None, signature: Some(Signature { - requires: Genus::Single(Forma("A")), - provides: Genus::Single(Forma("B")) + requires: Genus::Single(Forma::new("A")), + provides: Genus::Single(Forma::new("B")) }), elements: vec![], + span: Span::default(), }) ); @@ -99,13 +103,14 @@ second : C -> D assert_eq!( procedure, Ok(Procedure { - name: Identifier("second"), + name: Identifier::new("second"), parameters: None, signature: Some(Signature { - requires: Genus::Single(Forma("C")), - provides: Genus::Single(Forma("D")) + requires: Genus::Single(Forma::new("C")), + provides: Genus::Single(Forma::new("D")) }), elements: vec![], + span: Span::default(), }) ); } @@ -124,13 +129,14 @@ making_coffee(e) : Ingredients -> Coffee assert_eq!( procedure, Ok(Procedure { - name: Identifier("making_coffee"), - parameters: Some(vec![Identifier("e")]), + name: Identifier::new("making_coffee"), + parameters: Some(vec![Identifier::new("e")]), signature: Some(Signature { - requires: Genus::Single(Forma("Ingredients")), - provides: Genus::Single(Forma("Coffee")) + requires: Genus::Single(Forma::new("Ingredients")), + provides: Genus::Single(Forma::new("Coffee")) }), elements: vec![], + span: Span::default(), }) ); } @@ -156,36 +162,45 @@ This is the first one. assert_eq!( procedure, Ok(Procedure { - name: Identifier("first"), + name: Identifier::new("first"), parameters: None, signature: Some(Signature { - requires: Genus::Single(Forma("A")), - provides: Genus::Single(Forma("B")) + requires: Genus::Single(Forma::new("A")), + provides: Genus::Single(Forma::new("B")) }), elements: vec![ - Element::Title("The First"), - Element::Description(vec![Paragraph(vec![Descriptive::Text( - "This is the first one." - )])]), - Element::Steps(vec![ - Scope::DependentBlock { - ordinal: "1", - description: vec![Paragraph(vec![Descriptive::Text( - "Do the first thing in the first one." - )])], + Element::Title("The First", Span::default()), + Element::Description( + vec![Paragraph::new(vec![Descriptive::Text( + "This is the first one." + )])], + Span::default() + ), + Element::Steps( + vec![ + Scope::DependentBlock { + ordinal: "1", + description: vec![Paragraph::new(vec![Descriptive::Text( + "Do the first thing in the first one." + )])], - subscopes: vec![] - }, - Scope::DependentBlock { - ordinal: "2", - description: vec![Paragraph(vec![Descriptive::Text( - "Do the second thing in the first one." - )])], + subscopes: vec![], + span: Span::default(), + }, + Scope::DependentBlock { + ordinal: "2", + description: vec![Paragraph::new(vec![Descriptive::Text( + "Do the second thing in the first one." + )])], - subscopes: vec![] - } - ]) + subscopes: vec![], + span: Span::default(), + } + ], + Span::default() + ) ], + span: Span::default(), }) ); } @@ -211,46 +226,58 @@ This is the first one. assert_eq!( procedure, Ok(Procedure { - name: Identifier("first"), + name: Identifier::new("first"), parameters: None, signature: Some(Signature { - requires: Genus::Single(Forma("A")), - provides: Genus::Single(Forma("B")) + requires: Genus::Single(Forma::new("A")), + provides: Genus::Single(Forma::new("B")) }), elements: vec![ - Element::Title("The First"), - Element::Description(vec![Paragraph(vec![Descriptive::Text( - "This is the first one." - )])]), - Element::Steps(vec![ - Scope::DependentBlock { - ordinal: "1", - description: vec![Paragraph(vec![Descriptive::Text( - "Have you done the first thing in the first one?" - )])], - subscopes: vec![Scope::ResponseBlock { - responses: vec![ - Response { - value: "Yes", - condition: None - }, - Response { - value: "No", - condition: Some("but I have an excuse") - } - ] - }], - }, - Scope::DependentBlock { - ordinal: "2", - description: vec![Paragraph(vec![Descriptive::Text( - "Do the second thing in the first one." - )])], + Element::Title("The First", Span::default()), + Element::Description( + vec![Paragraph::new(vec![Descriptive::Text( + "This is the first one." + )])], + Span::default() + ), + Element::Steps( + vec![ + Scope::DependentBlock { + ordinal: "1", + description: vec![Paragraph::new(vec![Descriptive::Text( + "Have you done the first thing in the first one?" + )])], + subscopes: vec![Scope::ResponseBlock { + responses: vec![ + Response { + value: "Yes", + condition: None, + span: Span::default() + }, + Response { + value: "No", + condition: Some("but I have an excuse"), + span: Span::default() + } + ], + span: Span::default(), + }], + span: Span::default(), + }, + Scope::DependentBlock { + ordinal: "2", + description: vec![Paragraph::new(vec![Descriptive::Text( + "Do the second thing in the first one." + )])], - subscopes: vec![], - } - ]) + subscopes: vec![], + span: Span::default(), + } + ], + Span::default() + ) ], + span: Span::default(), }) ); } @@ -277,53 +304,66 @@ This is the first one. assert_eq!( procedure, Ok(Procedure { - name: Identifier("first"), + name: Identifier::new("first"), parameters: None, signature: Some(Signature { - requires: Genus::Single(Forma("A")), - provides: Genus::Single(Forma("B")) + requires: Genus::Single(Forma::new("A")), + provides: Genus::Single(Forma::new("B")) }), elements: vec![ - Element::Title("The First"), - Element::Description(vec![Paragraph(vec![Descriptive::Text( - "This is the first one." - )])]), - Element::Steps(vec![ - Scope::DependentBlock { - ordinal: "1", - description: vec![Paragraph(vec![Descriptive::Text( - "Have you done the first thing in the first one?" - )])], + Element::Title("The First", Span::default()), + Element::Description( + vec![Paragraph::new(vec![Descriptive::Text( + "This is the first one." + )])], + Span::default() + ), + Element::Steps( + vec![ + Scope::DependentBlock { + ordinal: "1", + description: vec![Paragraph::new(vec![Descriptive::Text( + "Have you done the first thing in the first one?" + )])], - subscopes: vec![Scope::DependentBlock { - ordinal: "a", - description: vec![Paragraph(vec![Descriptive::Text( - "Do the first thing. Then ask yourself if you are done:" + subscopes: vec![Scope::DependentBlock { + ordinal: "a", + description: vec![Paragraph::new(vec![Descriptive::Text( + "Do the first thing. Then ask yourself if you are done:" + )])], + subscopes: vec![Scope::ResponseBlock { + responses: vec![ + Response { + value: "Yes", + condition: None, + span: Span::default() + }, + Response { + value: "No", + condition: Some("but I have an excuse"), + span: Span::default() + } + ], + span: Span::default(), + }], + span: Span::default(), + }], + span: Span::default(), + }, + Scope::DependentBlock { + ordinal: "2", + description: vec![Paragraph::new(vec![Descriptive::Text( + "Do the second thing in the first one." )])], - subscopes: vec![Scope::ResponseBlock { - responses: vec![ - Response { - value: "Yes", - condition: None - }, - Response { - value: "No", - condition: Some("but I have an excuse") - } - ] - }] - }] - }, - Scope::DependentBlock { - ordinal: "2", - description: vec![Paragraph(vec![Descriptive::Text( - "Do the second thing in the first one." - )])], - subscopes: vec![], - } - ]) + subscopes: vec![], + span: Span::default(), + } + ], + Span::default() + ) ], + span: Span::default(), }) ); } @@ -361,136 +401,168 @@ fn realistic_procedure() { assert_eq!( procedure, Procedure { - name: Identifier("before_anesthesia"), + name: Identifier::new("before_anesthesia"), parameters: None, signature: None, elements: vec![ - Element::Title("Before induction of anaesthesia"), - Element::Steps(vec![ - Scope::DependentBlock { - ordinal: "1", - description: vec![Paragraph(vec![ - Descriptive::Text( - "Has the patient confirmed his/her identity, site, procedure," - ), - Descriptive::Text("and consent?") - ])], - subscopes: vec![Scope::ResponseBlock { - responses: vec![Response { - value: "Yes", - condition: None - }] - }], - }, - Scope::DependentBlock { - ordinal: "2", - description: vec![Paragraph(vec![Descriptive::Text( - "Is the site marked?" - )])], - subscopes: vec![Scope::ResponseBlock { - responses: vec![ - Response { + Element::Title("Before induction of anaesthesia", Span::default()), + Element::Steps( + vec![ + Scope::DependentBlock { + ordinal: "1", + description: vec![Paragraph::new(vec![ + Descriptive::Text( + "Has the patient confirmed his/her identity, site, procedure," + ), + Descriptive::Text("and consent?") + ])], + subscopes: vec![Scope::ResponseBlock { + responses: vec![Response { value: "Yes", - condition: None - }, - Response { - value: "Not Applicable", - condition: None - } - ] - }], - }, - Scope::DependentBlock { - ordinal: "3", - description: vec![Paragraph(vec![Descriptive::Text( - "Is the anaesthesia machine and medication check complete?" - )])], - subscopes: vec![Scope::ResponseBlock { - responses: vec![Response { - value: "Yes", - condition: None - }] - }], - }, - Scope::DependentBlock { - ordinal: "4", - description: vec![Paragraph(vec![Descriptive::Text( - "Is the pulse oximeter on the patient and functioning?" - )])], - subscopes: vec![Scope::ResponseBlock { - responses: vec![Response { - value: "Yes", - condition: None - }] - }], - }, - Scope::DependentBlock { - ordinal: "5", - description: vec![Paragraph(vec![Descriptive::Text( - "Does the patient have a:" - )])], - - subscopes: vec![ - Scope::ParallelBlock { - bullet: '-', - description: vec![Paragraph(vec![Descriptive::Text( - "Known allergy?" - )])], - subscopes: vec![Scope::ResponseBlock { - responses: vec![ - Response { - value: "No", - condition: None - }, - Response { - value: "Yes", - condition: None - } - ] + condition: None, + span: Span::default() }], - }, - Scope::ParallelBlock { - bullet: '-', - description: vec![Paragraph(vec![Descriptive::Text( - "Difficult airway or aspiration risk?" - )])], - subscopes: vec![Scope::ResponseBlock { - responses: vec![ - Response { - value: "No", - condition: None - }, - Response { - value: "Yes", - condition: Some("and equipment/assistance available") - } - ] + span: Span::default(), + }], + span: Span::default(), + }, + Scope::DependentBlock { + ordinal: "2", + description: vec![Paragraph::new(vec![Descriptive::Text( + "Is the site marked?" + )])], + subscopes: vec![Scope::ResponseBlock { + responses: vec![ + Response { + value: "Yes", + condition: None, + span: Span::default() + }, + Response { + value: "Not Applicable", + condition: None, + span: Span::default() + } + ], + span: Span::default(), + }], + span: Span::default(), + }, + Scope::DependentBlock { + ordinal: "3", + description: vec![Paragraph::new(vec![Descriptive::Text( + "Is the anaesthesia machine and medication check complete?" + )])], + subscopes: vec![Scope::ResponseBlock { + responses: vec![Response { + value: "Yes", + condition: None, + span: Span::default() }], - }, - Scope::ParallelBlock { - bullet: '-', - description: vec![Paragraph(vec![Descriptive::Text( - "Risk of blood loss > 500 mL?" - )])], - subscopes: vec![Scope::ResponseBlock { - responses: vec![ - Response { - value: "No", - condition: None - }, - Response { - value: "Yes", - condition: Some( - "and two IVs planned and fluids available" - ) - } - ] + span: Span::default(), + }], + span: Span::default(), + }, + Scope::DependentBlock { + ordinal: "4", + description: vec![Paragraph::new(vec![Descriptive::Text( + "Is the pulse oximeter on the patient and functioning?" + )])], + subscopes: vec![Scope::ResponseBlock { + responses: vec![Response { + value: "Yes", + condition: None, + span: Span::default() }], - } - ] - } - ]) - ] + span: Span::default(), + }], + span: Span::default(), + }, + Scope::DependentBlock { + ordinal: "5", + description: vec![Paragraph::new(vec![Descriptive::Text( + "Does the patient have a:" + )])], + + subscopes: vec![ + Scope::ParallelBlock { + bullet: '-', + description: vec![Paragraph::new(vec![Descriptive::Text( + "Known allergy?" + )])], + subscopes: vec![Scope::ResponseBlock { + responses: vec![ + Response { + value: "No", + condition: None, + span: Span::default() + }, + Response { + value: "Yes", + condition: None, + span: Span::default() + } + ], + span: Span::default(), + }], + span: Span::default(), + }, + Scope::ParallelBlock { + bullet: '-', + description: vec![Paragraph::new(vec![Descriptive::Text( + "Difficult airway or aspiration risk?" + )])], + subscopes: vec![Scope::ResponseBlock { + responses: vec![ + Response { + value: "No", + condition: None, + span: Span::default() + }, + Response { + value: "Yes", + condition: Some( + "and equipment/assistance available" + ), + span: Span::default(), + } + ], + span: Span::default(), + }], + span: Span::default(), + }, + Scope::ParallelBlock { + bullet: '-', + description: vec![Paragraph::new(vec![Descriptive::Text( + "Risk of blood loss > 500 mL?" + )])], + subscopes: vec![Scope::ResponseBlock { + responses: vec![ + Response { + value: "No", + condition: None, + span: Span::default() + }, + Response { + value: "Yes", + condition: Some( + "and two IVs planned and fluids available" + ), + span: Span::default(), + } + ], + span: Span::default(), + }], + span: Span::default(), + } + ], + span: Span::default(), + } + ], + Span::default() + ) + ], + span: Span::default(), } ); } @@ -515,48 +587,66 @@ label_the_specimens : assert_eq!( procedure, Ok(Procedure { - name: Identifier("label_the_specimens"), + name: Identifier::new("label_the_specimens"), parameters: None, signature: None, - elements: vec![Element::Steps(vec![Scope::DependentBlock { - ordinal: "1", - description: vec![Paragraph(vec![Descriptive::Text("Specimen labelling")])], + elements: vec![Element::Steps( + vec![Scope::DependentBlock { + ordinal: "1", + description: vec![Paragraph::new(vec![Descriptive::Text( + "Specimen labelling" + )])], + + subscopes: vec![ + Scope::AttributeBlock { + attributes: vec![Attribute::Role( + Identifier::new("nursing_team"), + Span::default() + )], + subscopes: vec![ + Scope::ParallelBlock { + bullet: '-', + description: vec![Paragraph::new(vec![Descriptive::Text( + "Label blood tests" + )])], - subscopes: vec![ - Scope::AttributeBlock { - attributes: vec![Attribute::Role(Identifier("nursing_team"))], - subscopes: vec![ - Scope::ParallelBlock { - bullet: '-', - description: vec![Paragraph(vec![Descriptive::Text( - "Label blood tests" - )])], + subscopes: vec![], + span: Span::default(), + }, + Scope::ParallelBlock { + bullet: '-', + description: vec![Paragraph::new(vec![Descriptive::Text( + "Label tissue samples" + )])], - subscopes: vec![], - }, - Scope::ParallelBlock { - bullet: '-', - description: vec![Paragraph(vec![Descriptive::Text( - "Label tissue samples" + subscopes: vec![], + span: Span::default(), + } + ], + span: Span::default(), + }, + Scope::AttributeBlock { + attributes: vec![Attribute::Role( + Identifier::new("admin_staff"), + Span::default() + )], + subscopes: vec![Scope::DependentBlock { + ordinal: "a", + description: vec![Paragraph::new(vec![Descriptive::Text( + "Prepare the envelopes" )])], subscopes: vec![], - } - ] - }, - Scope::AttributeBlock { - attributes: vec![Attribute::Role(Identifier("admin_staff"))], - subscopes: vec![Scope::DependentBlock { - ordinal: "a", - description: vec![Paragraph(vec![Descriptive::Text( - "Prepare the envelopes" - )])], - - subscopes: vec![], - }] - } - ], - }])], + span: Span::default(), + }], + span: Span::default(), + } + ], + span: Span::default(), + }], + Span::default() + )], + span: Span::default(), }) ); } @@ -597,125 +687,160 @@ before_leaving : assert_eq!( procedure, Procedure { - name: Identifier("before_leaving"), + name: Identifier::new("before_leaving"), parameters: None, signature: None, elements: vec![ - Element::Title("Before patient leaves operating room"), - Element::Steps(vec![ - Scope::DependentBlock { - ordinal: "1", - description: vec![Paragraph(vec![Descriptive::Text("Verbally confirm:")])], + Element::Title("Before patient leaves operating room", Span::default()), + Element::Steps( + vec![ + Scope::DependentBlock { + ordinal: "1", + description: vec![Paragraph::new(vec![Descriptive::Text( + "Verbally confirm:" + )])], - subscopes: vec![ - Scope::ParallelBlock { - bullet: '-', - description: vec![Paragraph(vec![Descriptive::Text( - "The name of the surgical procedure(s)." - )])], + subscopes: vec![ + Scope::ParallelBlock { + bullet: '-', + description: vec![Paragraph::new(vec![Descriptive::Text( + "The name of the surgical procedure(s)." + )])], - subscopes: vec![], - }, - Scope::ParallelBlock { - bullet: '-', - description: vec![Paragraph(vec![Descriptive::Text( - "Completion of instrument, sponge, and needle counts." - )])], - - subscopes: vec![], - }, - Scope::ParallelBlock { - bullet: '-', - description: vec![Paragraph(vec![Descriptive::Text( - "Specimen labelling" - )])], + subscopes: vec![], + span: Span::default(), + }, + Scope::ParallelBlock { + bullet: '-', + description: vec![Paragraph::new(vec![Descriptive::Text( + "Completion of instrument, sponge, and needle counts." + )])], - subscopes: vec![Scope::CodeBlock { - expressions: vec![Expression::Foreach( - vec![Identifier("specimen")], - Box::new(Expression::Variable(Identifier("specimens"))) - )], - subscopes: vec![Scope::AttributeBlock { - attributes: vec![Attribute::Role(Identifier( - "nursing_team" - ))], - subscopes: vec![Scope::DependentBlock { - ordinal: "a", - description: vec![Paragraph(vec![ + subscopes: vec![], + span: Span::default(), + }, + Scope::ParallelBlock { + bullet: '-', + description: vec![Paragraph::new(vec![Descriptive::Text( + "Specimen labelling" + )])], + + subscopes: vec![Scope::CodeBlock { + expressions: vec![Expression::Foreach( + vec![Identifier::new("specimen")], + Box::new(Expression::Variable( + Identifier::new("specimens"), + Span::default() + )), + Span::default(), + )], + subscopes: vec![Scope::AttributeBlock { + attributes: vec![Attribute::Role( + Identifier::new("nursing_team"), + Span::default(), + )], + subscopes: vec![Scope::DependentBlock { + ordinal: "a", + description: vec![Paragraph::new(vec![ Descriptive::Text( "Read specimen labels aloud, including patient" ), Descriptive::Text("name.") ])], - subscopes: vec![] - }] - }] - }] - }, - Scope::ParallelBlock { - bullet: '-', - description: vec![Paragraph(vec![Descriptive::Text( - "Whether there are any equipment problems to be addressed." - )])], + subscopes: vec![], + span: Span::default(), + }], + span: Span::default(), + }], + span: Span::default(), + }], + span: Span::default(), + }, + Scope::ParallelBlock { + bullet: '-', + description: vec![Paragraph::new(vec![Descriptive::Text( + "Whether there are any equipment problems to be addressed." + )])], - subscopes: vec![], - } - ] - }, - Scope::DependentBlock { - ordinal: "2", - description: vec![Paragraph(vec![Descriptive::Text( - "Post-operative care:" - )])], + subscopes: vec![], + span: Span::default(), + } + ], + span: Span::default(), + }, + Scope::DependentBlock { + ordinal: "2", + description: vec![Paragraph::new(vec![Descriptive::Text( + "Post-operative care:" + )])], - subscopes: vec![ - Scope::AttributeBlock { - attributes: vec![Attribute::Role(Identifier("surgeon"))], - subscopes: vec![Scope::DependentBlock { - ordinal: "a", - description: vec![Paragraph(vec![ + subscopes: vec![ + Scope::AttributeBlock { + attributes: vec![Attribute::Role( + Identifier::new("surgeon"), + Span::default() + )], + subscopes: vec![Scope::DependentBlock { + ordinal: "a", + description: vec![Paragraph::new(vec![ Descriptive::Text( "What are the key concerns for recovery and management" ), Descriptive::Text("of this patient?") ])], - subscopes: vec![], - }] - }, - Scope::AttributeBlock { - attributes: vec![Attribute::Role(Identifier("anesthetist"))], - subscopes: vec![Scope::DependentBlock { - ordinal: "b", - description: vec![Paragraph(vec![ + subscopes: vec![], + span: Span::default(), + }], + span: Span::default(), + }, + Scope::AttributeBlock { + attributes: vec![Attribute::Role( + Identifier::new("anesthetist"), + Span::default(), + )], + subscopes: vec![Scope::DependentBlock { + ordinal: "b", + description: vec![Paragraph::new(vec![ Descriptive::Text( "What are the key concerns for recovery and management" ), Descriptive::Text("of this patient?") ])], - subscopes: vec![], - }] - }, - Scope::AttributeBlock { - attributes: vec![Attribute::Role(Identifier("nursing_team"))], - subscopes: vec![Scope::DependentBlock { - ordinal: "c", - description: vec![Paragraph(vec![ + subscopes: vec![], + span: Span::default(), + }], + span: Span::default(), + }, + Scope::AttributeBlock { + attributes: vec![Attribute::Role( + Identifier::new("nursing_team"), + Span::default(), + )], + subscopes: vec![Scope::DependentBlock { + ordinal: "c", + description: vec![Paragraph::new(vec![ Descriptive::Text( "What are the key concerns for recovery and management" ), Descriptive::Text("of this patient?") ])], - subscopes: vec![], - }] - } - ], - } - ]) + subscopes: vec![], + span: Span::default(), + }], + span: Span::default(), + } + ], + span: Span::default(), + } + ], + Span::default() + ) ], + span: Span::default(), } ); } @@ -747,11 +872,12 @@ fn parallel_role_assignments() { ordinal, description: content, subscopes: scopes, + .. }) => { assert_eq!(ordinal, "5"); assert_eq!( content, - vec![Paragraph(vec![Descriptive::Text( + vec![Paragraph::new(vec![Descriptive::Text( "Review anticipated critical events." )])] ); @@ -762,9 +888,13 @@ fn parallel_role_assignments() { if let Scope::AttributeBlock { attributes, subscopes: substeps, + .. } = &scopes[0] { - assert_eq!(*attributes, vec![Attribute::Role(Identifier("surgeon"))]); + assert_eq!( + *attributes, + vec![Attribute::Role(Identifier::new("surgeon"), Span::default())] + ); assert_eq!(substeps.len(), 3); // a, b, c } else { panic!("Expected AttributedBlock for surgeon"); @@ -774,11 +904,15 @@ fn parallel_role_assignments() { if let Scope::AttributeBlock { attributes, subscopes: substeps, + .. } = &scopes[1] { assert_eq!( *attributes, - vec![Attribute::Role(Identifier("anaesthetist"))] + vec![Attribute::Role( + Identifier::new("anaesthetist"), + Span::default() + )] ); assert_eq!(substeps.len(), 1); // d } else { @@ -789,11 +923,15 @@ fn parallel_role_assignments() { if let Scope::AttributeBlock { attributes, subscopes: substeps, + .. } = &scopes[2] { assert_eq!( *attributes, - vec![Attribute::Role(Identifier("nursing_team"))] + vec![Attribute::Role( + Identifier::new("nursing_team"), + Span::default() + )] ); assert_eq!(substeps.len(), 2); // e, f } else { @@ -833,11 +971,12 @@ fn multiple_roles_with_dependent_substeps() { ordinal, description: content, subscopes: scopes, + .. }) => { assert_eq!(ordinal, "1"); assert_eq!( content, - vec![Paragraph(vec![Descriptive::Text( + vec![Paragraph::new(vec![Descriptive::Text( "Review surgical procedure" )])] ); @@ -847,9 +986,13 @@ fn multiple_roles_with_dependent_substeps() { if let Scope::AttributeBlock { attributes, subscopes: substeps, + .. } = &scopes[0] { - assert_eq!(*attributes, vec![Attribute::Role(Identifier("surgeon"))]); + assert_eq!( + *attributes, + vec![Attribute::Role(Identifier::new("surgeon"), Span::default())] + ); assert_eq!(substeps.len(), 3); } else { panic!("Expected AttributedBlock for surgeon"); @@ -859,11 +1002,15 @@ fn multiple_roles_with_dependent_substeps() { if let Scope::AttributeBlock { attributes, subscopes: substeps, + .. } = &scopes[1] { assert_eq!( *attributes, - vec![Attribute::Role(Identifier("anaesthetist"))] + vec![Attribute::Role( + Identifier::new("anaesthetist"), + Span::default() + )] ); assert_eq!(substeps.len(), 2); } else { @@ -874,11 +1021,15 @@ fn multiple_roles_with_dependent_substeps() { if let Scope::AttributeBlock { attributes, subscopes: substeps, + .. } = &scopes[2] { assert_eq!( *attributes, - vec![Attribute::Role(Identifier("nursing_team"))] + vec![Attribute::Role( + Identifier::new("nursing_team"), + Span::default() + )] ); assert_eq!(substeps.len(), 3); } else { @@ -930,50 +1081,64 @@ fn mixed_substeps_in_roles() { step, Scope::DependentBlock { ordinal: "1", - description: vec![Paragraph(vec![Descriptive::Text("Emergency response")])], + description: vec![Paragraph::new(vec![Descriptive::Text( + "Emergency response" + )])], subscopes: vec![Scope::AttributeBlock { - attributes: vec![Attribute::Role(Identifier("team_lead"))], + attributes: vec![Attribute::Role( + Identifier::new("team_lead"), + Span::default() + )], subscopes: vec![ Scope::DependentBlock { ordinal: "a", - description: vec![Paragraph(vec![Descriptive::Text("Assess situation")])], + description: vec![Paragraph::new(vec![Descriptive::Text( + "Assess situation" + )])], - subscopes: vec![] + subscopes: vec![], + span: Span::default(), }, Scope::DependentBlock { ordinal: "b", - description: vec![Paragraph(vec![Descriptive::Text( + description: vec![Paragraph::new(vec![Descriptive::Text( "Coordinate response" )])], subscopes: vec![ Scope::ParallelBlock { bullet: '-', - description: vec![Paragraph(vec![Descriptive::Text( + description: vec![Paragraph::new(vec![Descriptive::Text( "Monitor communications" )])], - subscopes: vec![] + subscopes: vec![], + span: Span::default(), }, Scope::ParallelBlock { bullet: '-', - description: vec![Paragraph(vec![Descriptive::Text( + description: vec![Paragraph::new(vec![Descriptive::Text( "Track resources" )])], - subscopes: vec![] + subscopes: vec![], + span: Span::default(), } - ] + ], + span: Span::default(), }, Scope::DependentBlock { ordinal: "c", - description: vec![Paragraph(vec![Descriptive::Text("File report")])], + description: vec![Paragraph::new(vec![Descriptive::Text("File report")])], - subscopes: vec![] + subscopes: vec![], + span: Span::default(), } - ] - }] + ], + span: Span::default(), + }], + span: Span::default(), } ); } @@ -995,24 +1160,31 @@ fn substeps_with_responses() { result, Ok(Scope::DependentBlock { ordinal: "1", - description: vec![Paragraph(vec![Descriptive::Text("Main step")])], + description: vec![Paragraph::new(vec![Descriptive::Text("Main step")])], subscopes: vec![Scope::DependentBlock { ordinal: "a", - description: vec![Paragraph(vec![Descriptive::Text("Substep with response")])], + description: vec![Paragraph::new(vec![Descriptive::Text( + "Substep with response" + )])], subscopes: vec![Scope::ResponseBlock { responses: vec![ Response { value: "Yes", condition: None, + span: Span::default() }, Response { value: "No", condition: None, + span: Span::default() }, - ] + ], + span: Span::default(), }], + span: Span::default(), }], + span: Span::default(), }) ); } @@ -1026,9 +1198,9 @@ fn naked_bindings() { let descriptive = input.read_descriptive(); assert_eq!( descriptive, - Ok(vec![Paragraph(vec![Descriptive::Binding( + Ok(vec![Paragraph::new(vec![Descriptive::Binding( Box::new(Descriptive::Text("What is the result?")), - vec![Identifier("answer")] + vec![Identifier::new("answer")] )])]) ); @@ -1038,10 +1210,10 @@ fn naked_bindings() { let descriptive = input.read_descriptive(); assert_eq!( descriptive, - Ok(vec![Paragraph(vec![ + Ok(vec![Paragraph::new(vec![ Descriptive::Binding( Box::new(Descriptive::Text("Enter your name")), - vec![Identifier("name")] + vec![Identifier::new("name")] ), Descriptive::Text("Continue with next step") ])]) @@ -1055,18 +1227,18 @@ fn naked_bindings() { let descriptive = input.read_descriptive(); assert_eq!( descriptive, - Ok(vec![Paragraph(vec![ + Ok(vec![Paragraph::new(vec![ Descriptive::Text("First"), Descriptive::Binding( Box::new(Descriptive::Application(Invocation { - target: Target::Local(Identifier("do_something")), + target: Target::Local(Identifier::new("do_something")), parameters: None, })), - vec![Identifier("result")] + vec![Identifier::new("result")] ), Descriptive::Binding( Box::new(Descriptive::Text("then describe the outcome")), - vec![Identifier("description")] + vec![Identifier::new("description")] ) ])]) ); @@ -1115,47 +1287,57 @@ second_section_second_procedure : source: None, header: None, body: Some(Technique::Procedures(vec![Procedure { - name: Identifier("main_procedure"), + name: Identifier::new("main_procedure"), parameters: None, signature: None, - elements: vec![Element::Steps(vec![ - Scope::SectionChunk { - numeral: "I", - title: Some(Paragraph(vec![Descriptive::Text("First Section")])), - body: Technique::Procedures(vec![ - Procedure { - name: Identifier("first_section_first_procedure"), - parameters: None, - signature: None, - elements: vec![Element::Title("One dot One")] - }, - Procedure { - name: Identifier("first_section_second_procedure"), - parameters: None, - signature: None, - elements: vec![Element::Title("One dot Two")] - } - ]), - }, - Scope::SectionChunk { - numeral: "II", - title: Some(Paragraph(vec![Descriptive::Text("Second Section")])), - body: Technique::Procedures(vec![ - Procedure { - name: Identifier("second_section_first_procedure"), - parameters: None, - signature: None, - elements: vec![Element::Title("Two dot One")] - }, - Procedure { - name: Identifier("second_section_second_procedure"), - parameters: None, - signature: None, - elements: vec![Element::Title("Two dot Two")] - } - ]), - }, - ])], + elements: vec![Element::Steps( + vec![ + Scope::SectionChunk { + numeral: "I", + title: Some(Paragraph::new(vec![Descriptive::Text("First Section")])), + body: Technique::Procedures(vec![ + Procedure { + name: Identifier::new("first_section_first_procedure"), + parameters: None, + signature: None, + elements: vec![Element::Title("One dot One", Span::default())], + span: Span::default(), + }, + Procedure { + name: Identifier::new("first_section_second_procedure"), + parameters: None, + signature: None, + elements: vec![Element::Title("One dot Two", Span::default())], + span: Span::default(), + } + ]), + span: Span::default(), + }, + Scope::SectionChunk { + numeral: "II", + title: Some(Paragraph::new(vec![Descriptive::Text("Second Section")])), + body: Technique::Procedures(vec![ + Procedure { + name: Identifier::new("second_section_first_procedure"), + parameters: None, + signature: None, + elements: vec![Element::Title("Two dot One", Span::default())], + span: Span::default(), + }, + Procedure { + name: Identifier::new("second_section_second_procedure"), + parameters: None, + signature: None, + elements: vec![Element::Title("Two dot Two", Span::default())], + span: Span::default(), + } + ]), + span: Span::default(), + }, + ], + Span::default() + )], + span: Span::default(), }])), } ); @@ -1192,7 +1374,7 @@ procedure_four : Concept -> Architecture // Verify that both sections contain their respective procedures if let Some(Technique::Procedures(procs)) = document.body { let main_proc = &procs[0]; - if let Some(Element::Steps(steps)) = main_proc + if let Some(Element::Steps(steps, _)) = main_proc .elements .first() { @@ -1206,8 +1388,8 @@ procedure_four : Concept -> Architecture } = &steps[0] { assert_eq!(section1_procs.len(), 2); - assert_eq!(section1_procs[0].name, Identifier("procedure_one")); - assert_eq!(section1_procs[1].name, Identifier("procedure_two")); + assert_eq!(section1_procs[0].name, Identifier::new("procedure_one")); + assert_eq!(section1_procs[1].name, Identifier::new("procedure_two")); } else { panic!("First section should contain procedures"); } @@ -1219,8 +1401,8 @@ procedure_four : Concept -> Architecture } = &steps[1] { assert_eq!(section2_procs.len(), 2); - assert_eq!(section2_procs[0].name, Identifier("procedure_three")); - assert_eq!(section2_procs[1].name, Identifier("procedure_four")); + assert_eq!(section2_procs[0].name, Identifier::new("procedure_three")); + assert_eq!(section2_procs[1].name, Identifier::new("procedure_four")); } else { panic!("Second section should contain procedures"); } @@ -1270,93 +1452,201 @@ III. Implementation source: None, header: None, body: Some(Technique::Procedures(vec![Procedure { - name: Identifier("main_procedure"), + name: Identifier::new("main_procedure"), parameters: None, signature: None, - elements: vec![Element::Steps(vec![ - Scope::SectionChunk { - numeral: "I", - title: Some(Paragraph(vec![Descriptive::Text("Concept")])), - body: Technique::Empty, - }, - Scope::SectionChunk { - numeral: "II", - title: Some(Paragraph(vec![Descriptive::Text( - "Requirements Definition and Architecture" - )])), - body: Technique::Procedures(vec![ - Procedure { - name: Identifier("requirements_and_architecture"), - parameters: None, - signature: Some(Signature { - requires: Genus::Single(Forma("Concept")), - provides: Genus::Naked(vec![ - Forma("Requirements"), - Forma("Architecture") - ]), - }), - elements: vec![Element::Steps(vec![ - Scope::DependentBlock { - ordinal: "2", - description: vec![Paragraph(vec![ - Descriptive::Text("Define Requirements"), - Descriptive::Application(Invocation { - target: Target::Local(Identifier( - "define_requirements" - )), - parameters: Some(vec![Expression::Variable( - Identifier("concept") - )]), - }), - ])], - - subscopes: vec![], - }, - Scope::DependentBlock { - ordinal: "3", - description: vec![Paragraph(vec![ - Descriptive::Text("Determine Architecture"), - Descriptive::Application(Invocation { - target: Target::Local(Identifier( - "determine_architecture" - )), - parameters: Some(vec![Expression::Variable( - Identifier("concept") - )]), - }), - ])], - - subscopes: vec![], - }, - ])], - }, - Procedure { - name: Identifier("define_requirements"), - parameters: None, - signature: Some(Signature { - requires: Genus::Single(Forma("Concept")), - provides: Genus::Single(Forma("Requirements")), - }), - elements: vec![], - }, - Procedure { - name: Identifier("determine_architecture"), - parameters: None, - signature: Some(Signature { - requires: Genus::Single(Forma("Concept")), - provides: Genus::Single(Forma("Architecture")), - }), - elements: vec![], - }, - ]), - }, - Scope::SectionChunk { - numeral: "III", - title: Some(Paragraph(vec![Descriptive::Text("Implementation")])), - body: Technique::Empty, - }, - ])], + elements: vec![Element::Steps( + vec![ + Scope::SectionChunk { + numeral: "I", + title: Some(Paragraph::new(vec![Descriptive::Text("Concept")])), + body: Technique::Empty, + span: Span::default(), + }, + Scope::SectionChunk { + numeral: "II", + title: Some(Paragraph::new(vec![Descriptive::Text( + "Requirements Definition and Architecture" + )])), + body: Technique::Procedures(vec![ + Procedure { + name: Identifier::new("requirements_and_architecture"), + parameters: None, + signature: Some(Signature { + requires: Genus::Single(Forma::new("Concept")), + provides: Genus::Naked(vec![ + Forma::new("Requirements"), + Forma::new("Architecture") + ]), + }), + elements: vec![Element::Steps( + vec![ + Scope::DependentBlock { + ordinal: "2", + description: vec![Paragraph::new(vec![ + Descriptive::Text("Define Requirements"), + Descriptive::Application(Invocation { + target: Target::Local(Identifier::new( + "define_requirements", + )), + parameters: Some(vec![ + Expression::Variable( + Identifier::new("concept"), + Span::default(), + ) + ]), + }), + ])], + + subscopes: vec![], + span: Span::default(), + }, + Scope::DependentBlock { + ordinal: "3", + description: vec![Paragraph::new(vec![ + Descriptive::Text("Determine Architecture"), + Descriptive::Application(Invocation { + target: Target::Local(Identifier::new( + "determine_architecture", + )), + parameters: Some(vec![ + Expression::Variable( + Identifier::new("concept"), + Span::default(), + ) + ]), + }), + ])], + + subscopes: vec![], + span: Span::default(), + }, + ], + Span::default() + )], + span: Span::default(), + }, + Procedure { + name: Identifier::new("define_requirements"), + parameters: None, + signature: Some(Signature { + requires: Genus::Single(Forma::new("Concept")), + provides: Genus::Single(Forma::new("Requirements")), + }), + elements: vec![], + span: Span::default(), + }, + Procedure { + name: Identifier::new("determine_architecture"), + parameters: None, + signature: Some(Signature { + requires: Genus::Single(Forma::new("Concept")), + provides: Genus::Single(Forma::new("Architecture")), + }), + elements: vec![], + span: Span::default(), + }, + ]), + span: Span::default(), + }, + Scope::SectionChunk { + numeral: "III", + title: Some(Paragraph::new(vec![Descriptive::Text("Implementation")])), + body: Technique::Empty, + span: Span::default(), + }, + ], + Span::default() + )], + span: Span::default(), }])), } ) } + +#[test] +fn spans_are_populated() { + let source = std::fs::read_to_string("tests/samples/KnownSpanLengths.tq").unwrap(); + + let mut input = Parser::new(); + input.initialize(&source); + + let metadata = input + .read_technique_header() + .unwrap(); + assert_eq!(metadata.span, Span::new(0, 42)); + + input.trim_whitespace(); + let procedure = input + .read_procedure() + .unwrap(); + assert_eq!(procedure.span, Span::new(43, 231)); + assert_eq!( + procedure + .name + .span, + Span::new(43, 5) + ); + + let params = procedure + .parameters + .as_ref() + .unwrap(); + assert_eq!(params[0].span, Span::new(49, 4)); + + if let Genus::Single(f) = &procedure + .signature + .as_ref() + .unwrap() + .requires + { + assert_eq!(f.span, Span::new(57, 6)); + } + if let Genus::Single(f) = &procedure + .signature + .as_ref() + .unwrap() + .provides + { + assert_eq!(f.span, Span::new(67, 8)); + } + + if let Element::Title(_, span) = &procedure.elements[0] { + assert_eq!(*span, Span::new(77, 14)); + } + if let Element::Description(_, span) = &procedure.elements[1] { + assert_eq!(*span, Span::new(92, 23)); + } + if let Element::Steps(scopes, span) = &procedure.elements[2] { + assert_eq!(*span, Span::new(119, 155)); + + if let Scope::DependentBlock { + span, subscopes, .. + } = &scopes[0] + { + assert_eq!(*span, Span::new(119, 41)); + if let Scope::AttributeBlock { + attributes, span, .. + } = &subscopes[0] + { + assert_eq!(*span, Span::new(151, 9)); + if let Attribute::Role(id, attr_span) = &attributes[0] { + assert_eq!(*attr_span, Span::new(151, 8)); + assert_eq!(id.span, Span::new(152, 7)); + } + } + } + if let Scope::DependentBlock { + span, subscopes, .. + } = &scopes[1] + { + assert_eq!(*span, Span::new(164, 110)); + + if let Scope::ResponseBlock { responses, .. } = &subscopes[0] { + assert_eq!(responses[0].span, Span::new(211, 6)); + assert_eq!(responses[1].span, Span::new(220, 21)); + assert_eq!(responses[2].span, Span::new(244, 29)); + } + } + } +} diff --git a/src/parsing/parser.rs b/src/parsing/parser.rs index bd96e717..8db7587f 100644 --- a/src/parsing/parser.rs +++ b/src/parsing/parser.rs @@ -26,115 +26,88 @@ pub fn parse_with_recovery<'i>( #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub enum ParsingError { // lowest priority - IllegalParserState(usize, usize), // offset, width (0 = unknown) - Unimplemented(usize, usize), - Unrecognized(usize, usize), // improve this - UnexpectedEndOfInput(usize, usize), - Expected(usize, usize, &'static str), // offset, width, expected - ExpectedMatchingChar(usize, usize, &'static str, char, char), - MissingParenthesis(usize, usize), + IllegalParserState(Span), + Unimplemented(Span), + Unrecognized(Span), + UnexpectedEndOfInput(Span), + Expected(Span, &'static str), + ExpectedMatchingChar(Span, &'static str, char, char), + MissingParenthesis(Span), // more specific errors - InvalidCharacter(usize, usize, char), - InvalidHeader(usize, usize), - InvalidIdentifier(usize, usize, String), - InvalidForma(usize, usize), - InvalidGenus(usize, usize), - InvalidSignature(usize, usize), - InvalidParameters(usize, usize), - InvalidDeclaration(usize, usize), - InvalidSection(usize, usize), - InvalidInvocation(usize, usize), - InvalidFunction(usize, usize), - InvalidCodeBlock(usize, usize), - InvalidStep(usize, usize), - InvalidSubstep(usize, usize), - InvalidAttribute(usize, usize), - InvalidResponse(usize, usize), - InvalidMultiline(usize, usize), - InvalidForeach(usize, usize), - InvalidIntegral(usize, usize), - InvalidQuantity(usize, usize), - InvalidQuantityDecimal(usize, usize), - InvalidQuantityUncertainty(usize, usize), - InvalidQuantityMagnitude(usize, usize), - InvalidQuantitySymbol(usize, usize), + InvalidCharacter(Span, char), + InvalidHeader(Span), + InvalidIdentifier(Span, String), + InvalidForma(Span), + InvalidGenus(Span), + InvalidSignature(Span), + InvalidParameters(Span), + InvalidDeclaration(Span), + InvalidSection(Span), + InvalidInvocation(Span), + InvalidFunction(Span), + InvalidCodeBlock(Span), + InvalidStep(Span), + InvalidSubstep(Span), + InvalidAttribute(Span), + InvalidResponse(Span), + InvalidMultiline(Span), + InvalidForeach(Span), + InvalidIntegral(Span), + InvalidQuantity(Span), + InvalidQuantityDecimal(Span), + InvalidQuantityUncertainty(Span), + InvalidQuantityMagnitude(Span), + InvalidQuantitySymbol(Span), // highest priority - UnclosedInterpolation(usize, usize), + UnclosedInterpolation(Span), } impl ParsingError { - pub fn offset(&self) -> usize { + pub fn span(&self) -> Span { match self { - ParsingError::IllegalParserState(offset, _) => *offset, - ParsingError::Unimplemented(offset, _) => *offset, - ParsingError::Unrecognized(offset, _) => *offset, - ParsingError::Expected(offset, _, _) => *offset, - ParsingError::ExpectedMatchingChar(offset, _, _, _, _) => *offset, - ParsingError::MissingParenthesis(offset, _) => *offset, - ParsingError::UnclosedInterpolation(offset, _) => *offset, - ParsingError::InvalidHeader(offset, _) => *offset, - ParsingError::InvalidCharacter(offset, _, _) => *offset, - ParsingError::UnexpectedEndOfInput(offset, _) => *offset, - ParsingError::InvalidIdentifier(offset, _, _) => *offset, - ParsingError::InvalidForma(offset, _) => *offset, - ParsingError::InvalidGenus(offset, _) => *offset, - ParsingError::InvalidSignature(offset, _) => *offset, - ParsingError::InvalidDeclaration(offset, _) => *offset, - ParsingError::InvalidParameters(offset, _) => *offset, - ParsingError::InvalidSection(offset, _) => *offset, - ParsingError::InvalidInvocation(offset, _) => *offset, - ParsingError::InvalidFunction(offset, _) => *offset, - ParsingError::InvalidCodeBlock(offset, _) => *offset, - 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, - ParsingError::InvalidQuantity(offset, _) => *offset, - ParsingError::InvalidQuantityDecimal(offset, _) => *offset, - ParsingError::InvalidQuantityUncertainty(offset, _) => *offset, - ParsingError::InvalidQuantityMagnitude(offset, _) => *offset, - ParsingError::InvalidQuantitySymbol(offset, _) => *offset, + ParsingError::IllegalParserState(span) + | ParsingError::Unimplemented(span) + | ParsingError::Unrecognized(span) + | ParsingError::UnexpectedEndOfInput(span) + | ParsingError::MissingParenthesis(span) + | ParsingError::InvalidHeader(span) + | ParsingError::InvalidForma(span) + | ParsingError::InvalidGenus(span) + | ParsingError::InvalidSignature(span) + | ParsingError::InvalidParameters(span) + | ParsingError::InvalidDeclaration(span) + | ParsingError::InvalidSection(span) + | ParsingError::InvalidInvocation(span) + | ParsingError::InvalidFunction(span) + | ParsingError::InvalidCodeBlock(span) + | ParsingError::InvalidStep(span) + | ParsingError::InvalidSubstep(span) + | ParsingError::InvalidAttribute(span) + | ParsingError::InvalidResponse(span) + | ParsingError::InvalidMultiline(span) + | ParsingError::InvalidForeach(span) + | ParsingError::InvalidIntegral(span) + | ParsingError::InvalidQuantity(span) + | ParsingError::InvalidQuantityDecimal(span) + | ParsingError::InvalidQuantityUncertainty(span) + | ParsingError::InvalidQuantityMagnitude(span) + | ParsingError::InvalidQuantitySymbol(span) + | ParsingError::UnclosedInterpolation(span) => *span, + ParsingError::Expected(span, _) + | ParsingError::ExpectedMatchingChar(span, _, _, _) + | ParsingError::InvalidCharacter(span, _) + | ParsingError::InvalidIdentifier(span, _) => *span, } } + pub fn offset(&self) -> usize { + self.span() + .offset + } + pub fn width(&self) -> usize { - match self { - ParsingError::IllegalParserState(_, width) => *width, - ParsingError::Unimplemented(_, width) => *width, - ParsingError::Unrecognized(_, width) => *width, - ParsingError::Expected(_, width, _) => *width, - ParsingError::ExpectedMatchingChar(_, width, _, _, _) => *width, - ParsingError::MissingParenthesis(_, width) => *width, - ParsingError::UnclosedInterpolation(_, width) => *width, - ParsingError::InvalidHeader(_, width) => *width, - ParsingError::InvalidCharacter(_, width, _) => *width, - ParsingError::UnexpectedEndOfInput(_, width) => *width, - ParsingError::InvalidIdentifier(_, width, _) => *width, - ParsingError::InvalidForma(_, width) => *width, - ParsingError::InvalidGenus(_, width) => *width, - ParsingError::InvalidSignature(_, width) => *width, - ParsingError::InvalidDeclaration(_, width) => *width, - ParsingError::InvalidParameters(_, width) => *width, - ParsingError::InvalidSection(_, width) => *width, - ParsingError::InvalidInvocation(_, width) => *width, - ParsingError::InvalidFunction(_, width) => *width, - ParsingError::InvalidCodeBlock(_, width) => *width, - 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, - ParsingError::InvalidQuantity(_, width) => *width, - ParsingError::InvalidQuantityDecimal(_, width) => *width, - ParsingError::InvalidQuantityUncertainty(_, width) => *width, - ParsingError::InvalidQuantityMagnitude(_, width) => *width, - ParsingError::InvalidQuantitySymbol(_, width) => *width, - } + self.span() + .length } } @@ -289,7 +262,7 @@ impl<'i> Parser<'i> { } } else { self.problems - .push(ParsingError::Unrecognized(self.offset, 0)); + .push(ParsingError::Unrecognized(Span::new(self.offset, 0))); self.skip_to_next_line(); } } @@ -311,7 +284,7 @@ impl<'i> Parser<'i> { if is_section(self.source) { match self.read_section() { Ok(section) => { - if let Some(Element::Steps(ref mut steps)) = procedure + if let Some(Element::Steps(ref mut steps, _)) = procedure .elements .last_mut() { @@ -320,7 +293,10 @@ impl<'i> Parser<'i> { // Create a new Steps element if one doesn't exist procedure .elements - .push(Element::Steps(vec![section])); + .push(Element::Steps( + vec![section], + Span::default(), + )); } } Err(error) => { @@ -361,7 +337,7 @@ impl<'i> Parser<'i> { } } else { self.problems - .push(ParsingError::Unrecognized(self.offset, 0)); + .push(ParsingError::Unrecognized(Span::new(self.offset, 0))); self.skip_to_next_line(); } } @@ -532,15 +508,13 @@ impl<'i> Parser<'i> { if !begun { return Err(ParsingError::Expected( - self.offset, - 0, + Span::new(self.offset, 0), "the start character", )); } if l == 0 { return Err(ParsingError::ExpectedMatchingChar( - self.offset, - 0, + Span::new(self.offset, 0), subject, start_char, end_char, @@ -579,8 +553,7 @@ impl<'i> Parser<'i> { .source .find(delimiter) .ok_or(ParsingError::Expected( - self.offset, - 0, + Span::new(self.offset, 0), "a starting delimiter", ))?; @@ -589,8 +562,7 @@ impl<'i> Parser<'i> { let end = self.source[start..] .find(delimiter) .ok_or(ParsingError::Expected( - self.offset, - 0, + Span::new(self.offset, 0), "the corresponding end delimiter", ))?; @@ -646,18 +618,19 @@ impl<'i> Parser<'i> { F: Fn(&mut Parser<'i>) -> Result, { let content = self.source; + let base = content.as_ptr() as usize; let mut results = Vec::new(); for chunk in content.split(delimiter) { let trimmed = chunk.trim_ascii(); if trimmed.is_empty() { return Err(ParsingError::Expected( - self.offset, - 0, + Span::new(self.offset, 0), "non-empty content between delimiters", )); } - let mut parser = self.subparser(0, trimmed); + let indent = trimmed.as_ptr() as usize - base; + let mut parser = self.subparser(indent, trimmed); results.push(function(&mut parser)?); self.problems .extend(parser.problems); @@ -717,6 +690,19 @@ impl<'i> Parser<'i> { parser } + /// `slice` must be a sub-slice of `self.source`. + fn span_of(&self, slice: &str) -> Span { + let inner = (slice.as_ptr() as usize) + - (self + .source + .as_ptr() as usize); + Span::new(self.offset + inner, slice.len()) + } + + fn span_since(&self, start: usize) -> Span { + Span::new(start, self.offset - start) + } + // because test cases and trivial single-line examples might omit an // ending newline, this also returns Ok if end of input is reached. fn require_newline(&mut self) -> Result<(), ParsingError> { @@ -733,7 +719,7 @@ impl<'i> Parser<'i> { } else if c.is_ascii_whitespace() { continue; } else { - return Err(ParsingError::InvalidCharacter(self.offset, 0, c)); + return Err(ParsingError::InvalidCharacter(Span::new(self.offset, 0), c)); } } @@ -757,7 +743,10 @@ impl<'i> Parser<'i> { Ok(1) } else { let error_offset = analyze_magic_line(inner.source); - Err(ParsingError::InvalidHeader(inner.offset + error_offset, 0)) + Err(ParsingError::InvalidHeader(Span::new( + inner.offset + error_offset, + 0, + ))) } }) } @@ -770,7 +759,7 @@ impl<'i> Parser<'i> { let cap = re .captures(inner.source) - .ok_or(ParsingError::InvalidHeader(inner.offset, 0))?; + .ok_or(ParsingError::InvalidHeader(Span::new(inner.offset, 0)))?; // Now to extracting the values we need. We get the license code from // the first capture. It must be present otherwise we don't have a @@ -779,10 +768,13 @@ impl<'i> Parser<'i> { let one = cap .get(1) - .ok_or(ParsingError::Expected(inner.offset, 0, "the license name"))?; + .ok_or(ParsingError::Expected( + Span::new(inner.offset, 0), + "the license name", + ))?; let result = validate_license(one.as_str()) - .ok_or(ParsingError::InvalidHeader(inner.offset, 0))?; + .ok_or(ParsingError::InvalidHeader(Span::new(inner.offset, 0)))?; let license = Some(result); // Now dig out the copyright, if present: @@ -790,7 +782,7 @@ impl<'i> Parser<'i> { let copyright = match cap.get(2) { Some(two) => { let result = validate_copyright(two.as_str()) - .ok_or(ParsingError::InvalidHeader(inner.offset, 0))?; + .ok_or(ParsingError::InvalidHeader(Span::new(inner.offset, 0)))?; Some(result) } None => None, @@ -806,26 +798,31 @@ impl<'i> Parser<'i> { let cap = re .captures(inner.source) - .ok_or(ParsingError::InvalidHeader(inner.offset, 0))?; + .ok_or(ParsingError::InvalidHeader(Span::new(inner.offset, 0)))?; let one = cap .get(1) - .ok_or(ParsingError::Expected(inner.offset, 0, "a domain name"))?; + .ok_or(ParsingError::Expected( + Span::new(inner.offset, 0), + "a domain name", + ))?; let result = validate_domain(one.as_str()) - .ok_or(ParsingError::InvalidHeader(inner.offset, 0))?; + .ok_or(ParsingError::InvalidHeader(Span::new(inner.offset, 0)))?; Ok(Some(result)) }) } fn read_technique_header(&mut self) -> Result, ParsingError> { + let start = self.offset; + // Process magic line let version = if is_magic_line(self.source) { let result = self.read_magic_line()?; self.require_newline()?; result } else { - Err(ParsingError::Expected(0, 0, "The % symbol"))? + Err(ParsingError::Expected(Span::new(0, 0), "The % symbol"))? }; // Process SPDX line @@ -851,6 +848,7 @@ impl<'i> Parser<'i> { license, copyright, domain, + span: self.span_since(start), }) } @@ -861,36 +859,34 @@ impl<'i> Parser<'i> { Some(c) => c, None => { let arrow_offset = analyze_malformed_signature(self.source); - return Err(ParsingError::InvalidSignature( + return Err(ParsingError::InvalidSignature(Span::new( self.offset + arrow_offset, 0, - )); + ))); } }; let one = cap .get(1) .ok_or(ParsingError::Expected( - self.offset, - 0, + Span::new(self.offset, 0), "a Genus for the requires", ))?; let two = cap .get(2) .ok_or(ParsingError::Expected( - self.offset, - 0, + Span::new(self.offset, 0), "a Genus for the provides", ))?; - let requires = validate_genus(one.as_str()).ok_or(ParsingError::InvalidGenus( - self.offset + one.start(), - one.len(), + let one_span = Span::new(self.offset + one.start(), one.len()); + let two_span = Span::new(self.offset + two.start(), two.len()); + let requires = validate_genus(one.as_str(), one_span).ok_or(ParsingError::InvalidGenus( + Span::new(one_span.offset, one_span.length), ))?; - let provides = validate_genus(two.as_str()).ok_or(ParsingError::InvalidGenus( - self.offset + two.start(), - two.len(), + let provides = validate_genus(two.as_str(), two_span).ok_or(ParsingError::InvalidGenus( + Span::new(two_span.offset, two_span.length), ))?; Ok(Signature { requires, provides }) @@ -913,28 +909,28 @@ impl<'i> Parser<'i> { let cap = re .captures(self.source) - .ok_or(ParsingError::InvalidDeclaration(self.offset, 0))?; + .ok_or(ParsingError::InvalidDeclaration(Span::new(self.offset, 0)))?; let one = cap .get(1) .ok_or(ParsingError::Expected( - self.offset, - 0, + Span::new(self.offset, 0), "an Identifier for the procedure declaration", ))?; let text = one.as_str(); let (name, parameters) = if let Some((before, list)) = text.split_once('(') { let before = before.trim(); - let name = validate_identifier(before).ok_or(ParsingError::InvalidIdentifier( - self.offset, - before.len(), - before.to_string(), - ))?; + let name = validate_identifier(before, self.span_of(before)).ok_or( + ParsingError::InvalidIdentifier( + Span::new(self.offset, before.len()), + before.to_string(), + ), + )?; // Extract parameters from parentheses if !list.ends_with(')') { - return Err(ParsingError::InvalidDeclaration(self.offset, 0)); + return Err(ParsingError::InvalidDeclaration(Span::new(self.offset, 0))); } let list = &list[..list.len() - 1].trim_ascii(); @@ -943,13 +939,11 @@ impl<'i> Parser<'i> { } else { let mut params = Vec::new(); for item in list.split(',') { - let param = validate_identifier(item.trim_ascii()).ok_or( + let trimmed = item.trim_ascii(); + let param = validate_identifier(trimmed, self.span_of(trimmed)).ok_or( ParsingError::InvalidIdentifier( - self.offset, - item.trim_ascii() - .len(), - item.trim_ascii() - .to_string(), + Span::new(self.offset, trimmed.len()), + trimmed.to_string(), ), )?; params.push(param); @@ -976,14 +970,18 @@ impl<'i> Parser<'i> { - text.as_ptr() as isize; let error_offset = self.offset + one.start() + first_param_pos as usize; let param_width = text.len() - first_param_pos as usize; - return Err(ParsingError::InvalidParameters(error_offset, param_width)); + return Err(ParsingError::InvalidParameters(Span::new( + error_offset, + param_width, + ))); } - let name = validate_identifier(text).ok_or(ParsingError::InvalidIdentifier( - self.offset, - text.len(), - text.to_string(), - ))?; + let name = validate_identifier(text, self.span_of(text)).ok_or( + ParsingError::InvalidIdentifier( + Span::new(self.offset, text.len()), + text.to_string(), + ), + )?; (name, None) }; @@ -1008,7 +1006,7 @@ impl<'i> Parser<'i> { Ok(title) } else { // we shouldn't have invoked this unless we have a title to parse! - Err(ParsingError::IllegalParserState(self.offset, 0)) + Err(ParsingError::IllegalParserState(Span::new(self.offset, 0))) } } @@ -1063,6 +1061,8 @@ impl<'i> Parser<'i> { &mut self, parser: &mut Parser<'i>, ) -> Result, ParsingError> { + let start = parser.offset; + // Extract the declaration with recovery let declaration = match self.parse_declaration(parser) { Ok(decl) => decl, @@ -1080,6 +1080,7 @@ impl<'i> Parser<'i> { } let content = parser.source; + let elem_start = parser.offset; if is_procedure_title(content) { match parser.take_block_lines( @@ -1093,7 +1094,10 @@ impl<'i> Parser<'i> { Ok(text) }, ) { - Ok(title) => elements.push(Element::Title(title)), + Ok(title) => { + let span = parser.span_since(elem_start); + elements.push(Element::Title(title, span)); + } Err(error) => { self.problems .push(error); @@ -1102,7 +1106,10 @@ impl<'i> Parser<'i> { } } else if is_code_block(content) { match parser.read_code_block() { - Ok(expressions) => elements.push(Element::CodeBlock(expressions)), + Ok(expressions) => { + let span = parser.span_since(elem_start); + elements.push(Element::CodeBlock(expressions, span)); + } Err(error) => { self.problems .push(error); @@ -1112,7 +1119,10 @@ impl<'i> Parser<'i> { } 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])), + Ok(attribute_block) => { + let span = parser.span_since(elem_start); + elements.push(Element::Steps(vec![attribute_block], span)); + } Err(error) => { self.problems .push(error); @@ -1121,7 +1131,10 @@ impl<'i> Parser<'i> { } } else { self.problems - .push(ParsingError::InvalidAttribute(parser.offset, content.len())); + .push(ParsingError::InvalidAttribute(Span::new( + parser.offset, + content.len(), + ))); parser.skip_to_next_line(); } } else if is_step(content) { @@ -1153,12 +1166,13 @@ impl<'i> Parser<'i> { } } if !steps.is_empty() { - elements.push(Element::Steps(steps)); + let span = parser.span_since(elem_start); + elements.push(Element::Steps(steps, span)); } } else if malformed_step_pattern(content) { // Store error but continue parsing self.problems - .push(ParsingError::InvalidStep(parser.offset, 0)); + .push(ParsingError::InvalidStep(Span::new(parser.offset, 0))); parser.skip_to_next_line(); } else { match parser.take_block_lines( @@ -1187,7 +1201,8 @@ impl<'i> Parser<'i> { ) { Ok(description) => { if !description.is_empty() { - elements.push(Element::Description(description)); + let span = parser.span_since(elem_start); + elements.push(Element::Description(description, span)); } } Err(error) => { @@ -1204,6 +1219,7 @@ impl<'i> Parser<'i> { parameters: declaration.1, signature: declaration.2, elements, + span: parser.span_since(start), }) } @@ -1282,6 +1298,7 @@ impl<'i> Parser<'i> { numeral, title, body: Technique::Empty, + span: Span::default(), }) } else if is_procedure_declaration(outer.source) { // Section contains procedures @@ -1308,6 +1325,7 @@ impl<'i> Parser<'i> { numeral, title, body: Technique::Procedures(procedures), + span: Span::default(), }) } else { // Section contains steps - parse as steps @@ -1334,6 +1352,7 @@ impl<'i> Parser<'i> { numeral, title, body: Technique::Steps(steps), + span: Span::default(), }) } }) @@ -1356,11 +1375,16 @@ impl<'i> Parser<'i> { let re = regex!(r"^\s*([IVX]+)\.\s*(.*)$"); let cap = re .captures(line) - .ok_or(ParsingError::InvalidSection(self.offset, 0))?; + .ok_or(ParsingError::InvalidSection(Span::new(self.offset, 0)))?; let numeral = match cap.get(1) { Some(one) => one.as_str(), - None => return Err(ParsingError::Expected(self.offset, 0, "section header")), + None => { + return Err(ParsingError::Expected( + Span::new(self.offset, 0), + "section header", + )) + } }; // Though section text appear as titles, they are in fact steps and so @@ -1379,7 +1403,7 @@ impl<'i> Parser<'i> { let paragraphs = parser.read_descriptive()?; if paragraphs.len() != 1 { - return Err(ParsingError::InvalidSection(self.offset, 0)); + return Err(ParsingError::InvalidSection(Span::new(self.offset, 0))); } let paragraph = paragraphs .into_iter() @@ -1409,7 +1433,7 @@ impl<'i> Parser<'i> { } let start = inner.offset; let expression = inner.read_expression()?; - let is_variable = if let Expression::Variable(_) = &expression { + let is_variable = if let Expression::Variable(_, _) = &expression { true } else { false @@ -1429,12 +1453,12 @@ impl<'i> Parser<'i> { .is_empty() { let width = inner.offset - start; - return Err(ParsingError::InvalidCodeBlock(start, width)); + return Err(ParsingError::InvalidCodeBlock(Span::new(start, width))); } } if expressions.is_empty() { - return Err(ParsingError::InvalidCodeBlock(inner.offset, 0)); + return Err(ParsingError::InvalidCodeBlock(Span::new(inner.offset, 0))); } Ok(expressions) @@ -1443,6 +1467,7 @@ impl<'i> Parser<'i> { fn read_expression(&mut self) -> Result, ParsingError> { self.trim_whitespace(); + let start = self.offset; let content = self .source .trim_ascii_start(); @@ -1457,27 +1482,30 @@ impl<'i> Parser<'i> { self.advance(tilde_pos + 1); // Move past ~ self.trim_whitespace(); } - return Err(ParsingError::MissingParenthesis(self.offset, 0)); + return Err(ParsingError::MissingParenthesis(Span::new(self.offset, 0))); } else if is_repeat_keyword(content) { self.read_repeat_expression() } else if is_foreach_keyword(content) { self.read_foreach_expression() } else if content.starts_with("foreach ") { // Malformed foreach expression - return Err(ParsingError::InvalidForeach(self.offset, 0)); + return Err(ParsingError::InvalidForeach(Span::new(self.offset, 0))); } else if content.starts_with('[') { self.read_tablet_expression() } else if is_numeric(content) { let numeric = self.read_numeric()?; - Ok(Expression::Number(numeric)) + let span = self.span_since(start); + Ok(Expression::Number(numeric, span)) } else if is_string_literal(content) { let parts = self.take_block_chars("a string literal", '"', '"', false, |inner| { inner.parse_string_pieces(inner.source) })?; - Ok(Expression::String(parts)) + let span = self.span_since(start); + Ok(Expression::String(parts, span)) } else if is_invocation(content) { let invocation = self.read_invocation()?; - Ok(Expression::Application(invocation)) + let span = self.span_since(start); + Ok(Expression::Application(invocation, span)) } else if is_function(content) { // Extract the entire text before the opening parenthesis self.trim_whitespace(); @@ -1487,7 +1515,7 @@ impl<'i> Parser<'i> { .unwrap(); // is_function() already checked let text = &content[0..paren]; - let target = validate_identifier(text); + let target = validate_identifier(text, self.span_of(text)); if target.is_none() { if text.contains(' ') || text.contains('<') || text.contains('>') { @@ -1496,9 +1524,15 @@ impl<'i> Parser<'i> { } else { content.len() }; - return Err(ParsingError::InvalidCodeBlock(self.offset, width)); + return Err(ParsingError::InvalidCodeBlock(Span::new( + self.offset, + width, + ))); } else { - return Err(ParsingError::InvalidFunction(self.offset, text.len())); + return Err(ParsingError::InvalidFunction(Span::new( + self.offset, + text.len(), + ))); } } @@ -1508,16 +1542,18 @@ impl<'i> Parser<'i> { let parameters = self.read_parameters()?; let function = Function { target, parameters }; - Ok(Expression::Execution(function)) + let span = self.span_since(start); + Ok(Expression::Execution(function, span)) } else { let identifier = self.read_identifier()?; - if self.source.starts_with('"') { - return Err(ParsingError::InvalidFunction( - self.offset - identifier.0.len(), - identifier.0.len(), - )); + if self + .source + .starts_with('"') + { + return Err(ParsingError::InvalidFunction(identifier.span)); } - Ok(Expression::Variable(identifier)) + let span = identifier.span; + Ok(Expression::Variable(identifier, span)) } } @@ -1526,6 +1562,8 @@ impl<'i> Parser<'i> { // - identifier // - (identifier, identifier, ...) + let start = self.offset; + // Skip "foreach" keyword - we already know it's there from starts_with check self.advance(7); self.trim_whitespace(); @@ -1541,13 +1579,14 @@ impl<'i> Parser<'i> { .unwrap() .is_ascii_whitespace() { - return Err(ParsingError::InvalidForeach(self.offset, 0)); + return Err(ParsingError::InvalidForeach(Span::new(self.offset, 0))); } self.trim_whitespace(); let expression = self.read_expression()?; - Ok(Expression::Foreach(identifiers, Box::new(expression))) + let span = self.span_since(start); + Ok(Expression::Foreach(identifiers, Box::new(expression), span)) } fn read_identifiers(&mut self) -> Result>, ParsingError> { @@ -1585,7 +1624,7 @@ impl<'i> Parser<'i> { } if identifiers.is_empty() { - return Err(ParsingError::InvalidForeach(outer.offset, 0)); + return Err(ParsingError::InvalidForeach(Span::new(outer.offset, 0))); } Ok(identifiers) @@ -1599,6 +1638,7 @@ impl<'i> Parser<'i> { fn read_repeat_expression(&mut self) -> Result, ParsingError> { // Parse "repeat " + let start = self.offset; self.advance(6); self.trim_whitespace(); @@ -1607,10 +1647,13 @@ impl<'i> Parser<'i> { // parsing. let expression = self.read_expression()?; - Ok(Expression::Repeat(Box::new(expression))) + let span = self.span_since(start); + Ok(Expression::Repeat(Box::new(expression), span)) } fn read_binding_expression(&mut self) -> Result, ParsingError> { + let start = self.offset; + // Parse the expression before the ~ operator let expression = self.take_until(&['~'], |inner| { let start_pos = inner.offset; @@ -1628,7 +1671,7 @@ impl<'i> Parser<'i> { + inner .source .len(); - Err(ParsingError::InvalidCodeBlock(start_pos, width)) + Err(ParsingError::InvalidCodeBlock(Span::new(start_pos, width))) } })?; @@ -1638,11 +1681,13 @@ impl<'i> Parser<'i> { let identifiers = self.read_identifiers()?; - Ok(Expression::Binding(Box::new(expression), identifiers)) + let span = self.span_since(start); + Ok(Expression::Binding(Box::new(expression), identifiers, span)) } fn read_tablet_expression(&mut self) -> Result, ParsingError> { - self.take_block_chars("a tablet", '[', ']', true, |outer| { + let start = self.offset; + let pairs = self.take_block_chars("a tablet", '[', ']', true, |outer| { let mut pairs = Vec::new(); loop { @@ -1661,8 +1706,7 @@ impl<'i> Parser<'i> { .starts_with('"') { return Err(ParsingError::Expected( - outer.offset, - 0, + Span::new(outer.offset, 0), "a string label for the field, in double-quotes", )); } @@ -1677,8 +1721,7 @@ impl<'i> Parser<'i> { .starts_with('=') { return Err(ParsingError::Expected( - outer.offset, - 0, + Span::new(outer.offset, 0), "a '=' after the field name to indicate what value is to be assigned to it", )); } @@ -1691,7 +1734,10 @@ impl<'i> Parser<'i> { let content = inner.source; if content.is_empty() { - return Err(ParsingError::Expected(inner.offset, 0, "value expression")); + return Err(ParsingError::Expected( + Span::new(inner.offset, 0), + "value expression", + )); }; inner.read_expression() @@ -1703,8 +1749,10 @@ impl<'i> Parser<'i> { outer.trim_whitespace(); } - Ok(Expression::Tablet(pairs)) - }) + Ok(pairs) + })?; + let span = self.span_since(start); + Ok(Expression::Tablet(pairs, span)) } fn parse_string_pieces(&mut self, raw: &'i str) -> Result>, ParsingError> { @@ -1756,10 +1804,10 @@ impl<'i> Parser<'i> { } None => { // Unmatched brace - point to the opening brace position - return Err(ParsingError::UnclosedInterpolation( + return Err(ParsingError::UnclosedInterpolation(Span::new( self.offset + absolute_brace_start, 0, - )); + ))); } } } else { @@ -1787,11 +1835,12 @@ impl<'i> Parser<'i> { Some(i) => &content[0..i], }; - let identifier = validate_identifier(possible).ok_or(ParsingError::InvalidIdentifier( - self.offset, - possible.len(), - possible.to_string(), - ))?; + let identifier = validate_identifier(possible, self.span_of(possible)).ok_or( + ParsingError::InvalidIdentifier( + Span::new(self.offset, possible.len()), + possible.to_string(), + ), + )?; self.advance(possible.len()); @@ -1809,7 +1858,7 @@ impl<'i> Parser<'i> { } else if is_numeric_quantity(content) { self.read_numeric_quantity() } else { - Err(ParsingError::InvalidQuantity(self.offset, 0)) + Err(ParsingError::InvalidQuantity(Span::new(self.offset, 0))) } } @@ -1824,7 +1873,7 @@ impl<'i> Parser<'i> { self.advance(content.len()); Ok(Numeric::Integral(amount)) } else { - Err(ParsingError::InvalidIntegral(self.offset, 0)) + Err(ParsingError::InvalidIntegral(Span::new(self.offset, 0))) } } @@ -1883,7 +1932,10 @@ impl<'i> Parser<'i> { .source .starts_with("10") { - return Err(ParsingError::InvalidQuantityMagnitude(self.offset, 0)); + return Err(ParsingError::InvalidQuantityMagnitude(Span::new( + self.offset, + 0, + ))); } self.advance(2); // Skip "10" @@ -1896,7 +1948,10 @@ impl<'i> Parser<'i> { } else if let Some(exp) = self.read_exponent_superscript() { Some(exp) } else { - return Err(ParsingError::InvalidQuantityMagnitude(self.offset, 0)); + return Err(ParsingError::InvalidQuantityMagnitude(Span::new( + self.offset, + 0, + ))); } } else { None @@ -1926,10 +1981,16 @@ impl<'i> Parser<'i> { self.advance(decimal_str.len()); Ok(decimal) } else { - Err(ParsingError::InvalidQuantityDecimal(self.offset, 0)) + Err(ParsingError::InvalidQuantityDecimal(Span::new( + self.offset, + 0, + ))) } } else { - Err(ParsingError::InvalidQuantityDecimal(self.offset, 0)) + Err(ParsingError::InvalidQuantityDecimal(Span::new( + self.offset, + 0, + ))) } } @@ -1943,10 +2004,16 @@ impl<'i> Parser<'i> { self.advance(decimal_str.len()); Ok(decimal) } else { - Err(ParsingError::InvalidQuantityUncertainty(self.offset, 0)) + Err(ParsingError::InvalidQuantityUncertainty(Span::new( + self.offset, + 0, + ))) } } else { - Err(ParsingError::InvalidQuantityUncertainty(self.offset, 0)) + Err(ParsingError::InvalidQuantityUncertainty(Span::new( + self.offset, + 0, + ))) } } @@ -1960,10 +2027,16 @@ impl<'i> Parser<'i> { self.advance(exp_str.len()); Ok(exp) } else { - Err(ParsingError::InvalidQuantityMagnitude(self.offset, 0)) + Err(ParsingError::InvalidQuantityMagnitude(Span::new( + self.offset, + 0, + ))) } } else { - Err(ParsingError::InvalidQuantityMagnitude(self.offset, 0)) + Err(ParsingError::InvalidQuantityMagnitude(Span::new( + self.offset, + 0, + ))) } } @@ -2006,15 +2079,18 @@ impl<'i> Parser<'i> { valid_end = byte_offset + ch.len_utf8(); } else { // Invalid character found - point directly at it - return Err(ParsingError::InvalidQuantitySymbol( + return Err(ParsingError::InvalidQuantitySymbol(Span::new( self.offset + byte_offset, ch.len_utf8(), - )); + ))); } } if valid_end == 0 { - return Err(ParsingError::InvalidQuantitySymbol(self.offset, 1)); + return Err(ParsingError::InvalidQuantitySymbol(Span::new( + self.offset, + 1, + ))); } let symbol = &self.source[..valid_end]; @@ -2030,11 +2106,15 @@ impl<'i> Parser<'i> { .source .trim(); if content.starts_with("https://") { - Ok(Target::Remote(External(content))) + Ok(Target::Remote(External { + value: content, + span: inner.span_of(content), + })) } else { - let identifier = validate_identifier(content).ok_or_else(|| { - ParsingError::InvalidInvocation(start_offset + 1, content.len()) - })?; + let identifier = + validate_identifier(content, inner.span_of(content)).ok_or_else(|| { + ParsingError::InvalidInvocation(Span::new(start_offset + 1, content.len())) + })?; Ok(Target::Local(identifier)) } }) @@ -2055,18 +2135,18 @@ impl<'i> Parser<'i> { 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; // Parse ordinal let re = regex!(r"^\s*(\d+)\.\s+"); let cap = re .captures(outer.source) - .ok_or(ParsingError::InvalidStep(outer.offset, 0))?; + .ok_or(ParsingError::InvalidStep(Span::new(outer.offset, 0)))?; let number = cap .get(1) .ok_or(ParsingError::Expected( - outer.offset, - 0, + Span::new(outer.offset, 0), "the ordinal Step number", ))? .as_str(); @@ -2087,6 +2167,7 @@ impl<'i> Parser<'i> { ordinal: number, description: text, subscopes: scopes, + span: outer.span_since(start), }); }) } @@ -2095,13 +2176,14 @@ impl<'i> Parser<'i> { fn read_step_parallel(&mut self) -> Result, ParsingError> { self.take_block_lines(is_step_parallel, is_step_parallel, |outer| { outer.trim_whitespace(); + let start = outer.offset; // Parse bullet if !outer .source .starts_with('-') { - return Err(ParsingError::IllegalParserState(outer.offset, 0)); + return Err(ParsingError::IllegalParserState(Span::new(outer.offset, 0))); } outer.advance(1); // skip over '-' outer.trim_whitespace(); @@ -2115,6 +2197,7 @@ impl<'i> Parser<'i> { bullet: '-', description: text, subscopes: scopes, + span: outer.span_since(start), }); }) } @@ -2125,17 +2208,17 @@ impl<'i> Parser<'i> { is_substep_dependent, |line| is_substep_dependent(line), |outer| { + let start = outer.offset; let content = outer.source; let re = regex!(r"^\s*([a-hj-uwy])\.\s+"); let cap = re .captures(content) - .ok_or(ParsingError::InvalidStep(outer.offset, 0))?; + .ok_or(ParsingError::InvalidStep(Span::new(outer.offset, 0)))?; let letter = cap .get(1) .ok_or(ParsingError::Expected( - outer.offset, - 0, + Span::new(outer.offset, 0), "the ordinal Sub-Step letter", ))? .as_str(); @@ -2158,6 +2241,7 @@ impl<'i> Parser<'i> { ordinal: letter, description: text, subscopes: scopes, + span: outer.span_since(start), }) }, ) @@ -2169,10 +2253,11 @@ impl<'i> Parser<'i> { is_substep_parallel, |line| is_substep_parallel(line), |outer| { + let start = outer.offset; let re = regex!(r"^\s*-\s+"); let zero = re .find(outer.source) - .ok_or(ParsingError::InvalidStep(outer.offset, 0))?; + .ok_or(ParsingError::InvalidStep(Span::new(outer.offset, 0)))?; // Skip past the dash and space let l = zero.len(); @@ -2189,6 +2274,7 @@ impl<'i> Parser<'i> { bullet: '-', description: text, subscopes: scopes, + span: outer.span_since(start), }) }, ) @@ -2200,17 +2286,17 @@ impl<'i> Parser<'i> { is_subsubstep_dependent, |line| is_subsubstep_dependent(line), |outer| { + let start = outer.offset; let content = outer.source; let re = regex!(r"^\s*([ivx]+)\.\s+"); let cap = re .captures(content) - .ok_or(ParsingError::InvalidStep(outer.offset, 0))?; + .ok_or(ParsingError::InvalidStep(Span::new(outer.offset, 0)))?; let numeral = cap .get(1) .ok_or(ParsingError::Expected( - outer.offset, - 0, + Span::new(outer.offset, 0), "the ordinal roman numeral indicating a sub-substep", ))? .as_str(); @@ -2230,6 +2316,7 @@ impl<'i> Parser<'i> { ordinal: numeral, description: text, subscopes: scopes, + span: outer.span_since(start), }) }, ) @@ -2262,15 +2349,18 @@ 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 para_start = outer.offset; let expressions = outer.read_code_block()?; + let para_span = outer.span_since(para_start); for expr in expressions { if let Expression::Separator = expr { continue; } - results.push(Paragraph(vec![Descriptive::CodeInline(expr)])); + results.push(Paragraph(vec![Descriptive::CodeInline(expr)], para_span)); } } else { // Paragraph container + let para_start = outer.offset; let descriptives = outer.take_paragraph(|parser| { let mut content = vec![]; @@ -2293,7 +2383,10 @@ impl<'i> Parser<'i> { .starts_with("```") { // Multiline blocks are not allowed in descriptive text - return Err(ParsingError::InvalidMultiline(parser.offset, 0)); + return Err(ParsingError::InvalidMultiline(Span::new( + parser.offset, + 0, + ))); } else if c == '<' { let invocation = parser.read_invocation()?; parser.trim_whitespace(); @@ -2310,7 +2403,7 @@ impl<'i> Parser<'i> { .starts_with(',') { return Err(ParsingError::MissingParenthesis( - start_pos, 0, + Span::new(start_pos, 0), )); } @@ -2330,8 +2423,7 @@ impl<'i> Parser<'i> { // Check for invalid multiline patterns in text if content.contains("```") { return Err(ParsingError::InvalidMultiline( - inner.offset, - 0, + Span::new(inner.offset, 0), )); } Ok(content) @@ -2350,7 +2442,7 @@ impl<'i> Parser<'i> { .starts_with(',') { return Err(ParsingError::MissingParenthesis( - start_pos, 0, + Span::new(start_pos, 0), )); } @@ -2368,7 +2460,7 @@ impl<'i> Parser<'i> { })?; if !descriptives.is_empty() { - results.push(Paragraph(descriptives)); + results.push(Paragraph(descriptives, outer.span_since(para_start))); } } } @@ -2381,7 +2473,15 @@ impl<'i> Parser<'i> { /// Parse enum responses like 'Yes' | 'No' | 'Not Applicable' fn read_responses(&mut self) -> Result>, ParsingError> { self.take_split_by('|', |inner| { - validate_response(inner.source).ok_or(ParsingError::InvalidResponse(inner.offset, 0)) + let mut resp = validate_response(inner.source) + .ok_or(ParsingError::InvalidResponse(Span::new(inner.offset, 0)))?; + resp.span = Span::new( + inner.offset, + inner + .source + .len(), + ); + Ok(resp) }) } @@ -2424,7 +2524,7 @@ impl<'i> Parser<'i> { .trim_ascii() .is_empty() { - return Err(ParsingError::InvalidMultiline(self.offset, 0)); + return Err(ParsingError::InvalidMultiline(Span::new(self.offset, 0))); } result.push(after) @@ -2464,27 +2564,29 @@ impl<'i> Parser<'i> { break; } + let param_start = outer.offset; if content.starts_with("```") { let (lang, lines) = outer .take_block_delimited("```", |inner| inner.parse_multiline_content()) .map_err(|err| match err { - ParsingError::Expected( - offset, - _, - "the corresponding end delimiter", - ) => ParsingError::InvalidMultiline(offset, 0), + ParsingError::Expected(span, "the corresponding end delimiter") => { + ParsingError::InvalidMultiline(Span::new(span.offset, 0)) + } _ => err, })?; - params.push(Expression::Multiline(lang, lines)); + let span = outer.span_since(param_start); + params.push(Expression::Multiline(lang, lines, span)); } else if content.starts_with("\"") { let parts = outer.take_block_chars("a string literal", '"', '"', false, |inner| { inner.parse_string_pieces(inner.source) })?; - params.push(Expression::String(parts)); + let span = outer.span_since(param_start); + params.push(Expression::String(parts, span)); } else if is_numeric_quantity(content) { let numeric = outer.read_numeric_quantity()?; - params.push(Expression::Number(numeric)); + let span = outer.span_since(param_start); + params.push(Expression::Number(numeric, span)); } else if is_numeric_integral(content) || content .as_bytes() @@ -2497,10 +2599,12 @@ impl<'i> Parser<'i> { .is_some_and(|b| b.is_ascii_digit()) { let decimal = outer.read_decimal_part()?; - params.push(Expression::Number(Numeric::Integral(decimal.number))); + let span = outer.span_since(param_start); + params.push(Expression::Number(Numeric::Integral(decimal.number), span)); } else { let name = outer.read_identifier()?; - params.push(Expression::Variable(name)); + let span = name.span; + params.push(Expression::Variable(name, span)); } // Handle comma separation @@ -2564,47 +2668,54 @@ impl<'i> Parser<'i> { // Check if it's the special @* "reset attribute" role if trimmed == "@*" { - let identifier = Identifier("*"); - attributes.push(Attribute::Role(identifier)); + let star = &trimmed[1..]; + let identifier = Identifier { + value: "*", + span: inner.span_of(star), + }; + attributes.push(Attribute::Role(identifier, inner.span_of(trimmed))); } // Check if it's a regular role '@' else if let Some(captures) = regex!(r"^@([a-z][a-z0-9_]*)$").captures(trimmed) { let role_name = captures .get(1) - .ok_or(ParsingError::Expected(inner.offset, 0, "role name after @"))? + .ok_or(ParsingError::Expected( + Span::new(inner.offset, 0), + "role name after @", + ))? .as_str(); - let identifier = - validate_identifier(role_name).ok_or(ParsingError::InvalidIdentifier( - inner.offset, - role_name.len(), + let identifier = validate_identifier(role_name, inner.span_of(role_name)) + .ok_or(ParsingError::InvalidIdentifier( + Span::new(inner.offset, role_name.len()), role_name.to_string(), ))?; - attributes.push(Attribute::Role(identifier)); + attributes.push(Attribute::Role(identifier, inner.span_of(trimmed))); } // Check if it's a place '^' else if let Some(captures) = regex!(r"^\^([a-z][a-z0-9_]*)$").captures(trimmed) { let place_name = captures .get(1) .ok_or(ParsingError::Expected( - inner.offset, - 0, + Span::new(inner.offset, 0), "place name after ^", ))? .as_str(); - let identifier = - validate_identifier(place_name).ok_or(ParsingError::InvalidIdentifier( - inner.offset, - place_name.len(), + let identifier = validate_identifier(place_name, inner.span_of(place_name)) + .ok_or(ParsingError::InvalidIdentifier( + Span::new(inner.offset, place_name.len()), place_name.to_string(), ))?; - attributes.push(Attribute::Place(identifier)); + attributes.push(Attribute::Place(identifier, inner.span_of(trimmed))); } else { // 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())); + return Err(ParsingError::InvalidAttribute(Span::new( + inner.offset, + line.len(), + ))); } else { - return Err(ParsingError::InvalidStep(inner.offset, 0)); + return Err(ParsingError::InvalidStep(Span::new(inner.offset, 0))); } } } @@ -2631,7 +2742,10 @@ impl<'i> Parser<'i> { let block = self.read_attribute_scope()?; scopes.push(block); } else { - return Err(ParsingError::InvalidAttribute(self.offset, content.len())); + return Err(ParsingError::InvalidAttribute(Span::new( + self.offset, + content.len(), + ))); } } else if is_substep_dependent(content) { let block = self.read_substep_dependent()?; @@ -2652,12 +2766,16 @@ impl<'i> Parser<'i> { let block = self.read_code_scope()?; scopes.push(block); } else if malformed_step_pattern(content) { - return Err(ParsingError::InvalidSubstep(self.offset, 0)); + return Err(ParsingError::InvalidSubstep(Span::new(self.offset, 0))); } else if malformed_response_pattern(content) { - return Err(ParsingError::InvalidResponse(self.offset, 0)); + return Err(ParsingError::InvalidResponse(Span::new(self.offset, 0))); } else if is_enum_response(content) { + let responses_start = self.offset; let responses = self.read_responses()?; - scopes.push(Scope::ResponseBlock { responses }); + scopes.push(Scope::ResponseBlock { + responses, + span: self.span_since(responses_start), + }); } else { break; } @@ -2668,12 +2786,14 @@ impl<'i> Parser<'i> { /// Parse an attribute block (role or place assignment) with its subscopes fn read_attribute_scope(&mut self) -> Result, ParsingError> { self.take_block_lines(is_attribute_assignment, is_attribute_assignment, |outer| { + let start = outer.offset; let attributes = outer.read_attributes()?; let subscopes = outer.read_scopes()?; Ok(Scope::AttributeBlock { attributes, subscopes, + span: outer.span_since(start), }) }) } @@ -2689,12 +2809,14 @@ impl<'i> Parser<'i> { false // Never stop - consume all remaining content }, |outer| { + let start = outer.offset; let expressions = outer.read_code_block()?; let subscopes = outer.read_scopes()?; Ok(Scope::CodeBlock { expressions, subscopes, + span: outer.span_since(start), }) }, ) diff --git a/src/problem/messages.rs b/src/problem/messages.rs index 0b9e03a9..a1a4a8c0 100644 --- a/src/problem/messages.rs +++ b/src/problem/messages.rs @@ -4,26 +4,26 @@ use technique::{formatting::Render, language::*, parsing::ParsingError}; /// Generate problem and detail messages for parsing errors using AST construction pub fn generate_error_message<'i>(error: &ParsingError, renderer: &dyn Render) -> (String, String) { match error { - ParsingError::IllegalParserState(_, _) => ( + ParsingError::IllegalParserState(_) => ( "Illegal parser state".to_string(), "Internal parser error. This should not have happened! Sorry.".to_string(), ), - ParsingError::Unimplemented(_, _) => ( + ParsingError::Unimplemented(_) => ( "Feature not yet implemented".to_string(), "This feature is planned but not yet available.".to_string(), ), - ParsingError::Unrecognized(_, _) => ( + ParsingError::Unrecognized(_) => ( "Unrecognized input".to_string(), "The parser encountered unexpected content".to_string(), ), - ParsingError::Expected(_, _, value) => ( + ParsingError::Expected(_, value) => ( format!("Expected {}", value), format!( "The parser was looking for {} but found something else.", value ), ), - ParsingError::ExpectedMatchingChar(_, _, subject, start, end) => ( + ParsingError::ExpectedMatchingChar(_, subject, start, end) => ( format!("Expected matching character '{}'", end), format!( r#" @@ -35,13 +35,13 @@ there was no more input remaining in the current scope. .trim_ascii() .to_string(), ), - ParsingError::MissingParenthesis(_, _) => { + ParsingError::MissingParenthesis(_) => { let examples = vec![Descriptive::Binding( Box::new(Descriptive::Application(Invocation { - target: Target::Local(Identifier("mix_pangalactic_gargle_blaster")), + target: Target::Local(Identifier::new("mix_pangalactic_gargle_blaster")), parameters: None, })), - vec![Identifier("zaphod"), Identifier("trillian")], + vec![Identifier::new("zaphod"), Identifier::new("trillian")], )]; ( @@ -59,7 +59,7 @@ enclose those names in parenthesis. For example: .to_string(), ) } - ParsingError::UnclosedInterpolation(_, _) => ( + ParsingError::UnclosedInterpolation(_) => ( "Unclosed string interpolation".to_string(), r#" Every '{' that starts an interpolation within a string must have a @@ -69,7 +69,7 @@ literal resumes. .trim_ascii() .to_string(), ), - ParsingError::InvalidHeader(_, _) => { + ParsingError::InvalidHeader(_) => { // Format the sample metadata using the same code as the formatter let mut formatted_example = String::new(); formatted_example @@ -115,39 +115,43 @@ to be used when rendering the Technique. Common domains include ), ) } - ParsingError::InvalidCharacter(_, _, c) => ( + ParsingError::InvalidCharacter(_, c) => ( format!("Invalid character '{}'", c), "This character is not allowed here.".to_string(), ), - ParsingError::UnexpectedEndOfInput(_, _) => ( + ParsingError::UnexpectedEndOfInput(_) => ( "Unexpected end of input".to_string(), "The file ended before the parser expected it to".to_string(), ), - ParsingError::InvalidIdentifier(_, _, _) => { + ParsingError::InvalidIdentifier(_, _) => { let examples = vec![ Procedure { - name: Identifier("make_coffee"), + name: Identifier::new("make_coffee"), parameters: None, signature: None, elements: Vec::new(), + span: Span::default(), }, Procedure { - name: Identifier("attempt1"), + name: Identifier::new("attempt1"), parameters: None, signature: None, elements: Vec::new(), + span: Span::default(), }, Procedure { - name: Identifier("i"), + name: Identifier::new("i"), parameters: None, signature: None, elements: Vec::new(), + span: Span::default(), }, Procedure { - name: Identifier("l33t_hax0r"), + name: Identifier::new("l33t_hax0r"), parameters: None, signature: None, elements: Vec::new(), + span: Span::default(), }, ]; @@ -172,11 +176,11 @@ letters, numbers, and underscores. Valid examples include: .to_string(), ) } - ParsingError::InvalidForma(_, _) => { + ParsingError::InvalidForma(_) => { let examples = vec![ - Forma("Coffee"), - Forma("Ingredients"), - Forma("PatientRecord"), + Forma::new("Coffee"), + Forma::new("Ingredients"), + Forma::new("PatientRecord"), ]; ( @@ -198,12 +202,12 @@ For example: .to_string(), ) } - ParsingError::InvalidGenus(_, _) => { + ParsingError::InvalidGenus(_) => { let examples = vec![ - Genus::Single(Forma("Coffee")), - Genus::Tuple(vec![Forma("Beans"), Forma("Water")]), - Genus::Naked(vec![Forma("Beans"), Forma("Water")]), - Genus::List(Forma("Patient")), + Genus::Single(Forma::new("Coffee")), + Genus::Tuple(vec![Forma::new("Beans"), Forma::new("Water")]), + Genus::Naked(vec![Forma::new("Beans"), Forma::new("Water")]), + Genus::List(Forma::new("Patient")), Genus::Unit, ]; @@ -234,19 +238,19 @@ doesn't have an input or result, per se. .to_string(), ) } - ParsingError::InvalidSignature(_, _) => { + ParsingError::InvalidSignature(_) => { let examples = vec![ Signature { - requires: Genus::Single(Forma("A")), - provides: Genus::Single(Forma("B")), + requires: Genus::Single(Forma::new("A")), + provides: Genus::Single(Forma::new("B")), }, Signature { - requires: Genus::Tuple(vec![Forma("Beans"), Forma("Milk")]), - provides: Genus::Single(Forma("Coffee")), + requires: Genus::Tuple(vec![Forma::new("Beans"), Forma::new("Milk")]), + provides: Genus::Single(Forma::new("Coffee")), }, Signature { - requires: Genus::List(Forma("FunctionalRequirement")), - provides: Genus::Single(Forma("Architecture")), + requires: Genus::List(Forma::new("FunctionalRequirement")), + provides: Genus::Single(Forma::new("Architecture")), }, ]; @@ -272,70 +276,78 @@ this form. .to_string(), ) } - ParsingError::InvalidDeclaration(_, _) => { + ParsingError::InvalidDeclaration(_) => { let examples = vec![ Procedure { - name: Identifier("f"), + name: Identifier::new("f"), parameters: None, signature: None, elements: Vec::new(), + span: Span::default(), }, Procedure { - name: Identifier("implementation"), + name: Identifier::new("implementation"), parameters: None, signature: None, elements: Vec::new(), + span: Span::default(), }, Procedure { - name: Identifier("make_coffee"), + name: Identifier::new("make_coffee"), parameters: None, signature: None, elements: Vec::new(), + span: Span::default(), }, Procedure { - name: Identifier("f"), + name: Identifier::new("f"), parameters: None, signature: Some(Signature { - requires: Genus::Single(Forma("A")), - provides: Genus::Single(Forma("B")), + requires: Genus::Single(Forma::new("A")), + provides: Genus::Single(Forma::new("B")), }), elements: Vec::new(), + span: Span::default(), }, Procedure { - name: Identifier("implementation"), + name: Identifier::new("implementation"), parameters: None, signature: Some(Signature { - requires: Genus::Single(Forma("Design")), - provides: Genus::Single(Forma("Product")), + requires: Genus::Single(Forma::new("Design")), + provides: Genus::Single(Forma::new("Product")), }), elements: Vec::new(), + span: Span::default(), }, Procedure { - name: Identifier("make_coffee"), + name: Identifier::new("make_coffee"), parameters: None, signature: Some(Signature { - requires: Genus::Naked(vec![Forma("Beans"), Forma("Milk")]), - provides: Genus::Single(Forma("Coffee")), + requires: Genus::Naked(vec![Forma::new("Beans"), Forma::new("Milk")]), + provides: Genus::Single(Forma::new("Coffee")), }), elements: Vec::new(), + span: Span::default(), }, Procedure { - name: Identifier("make_coffee"), + name: Identifier::new("make_coffee"), parameters: None, signature: Some(Signature { - requires: Genus::Tuple(vec![Forma("Beans"), Forma("Milk")]), - provides: Genus::Single(Forma("Coffee")), + requires: Genus::Tuple(vec![Forma::new("Beans"), Forma::new("Milk")]), + provides: Genus::Single(Forma::new("Coffee")), }), elements: Vec::new(), + span: Span::default(), }, Procedure { - name: Identifier("make_coffee"), - parameters: Some(vec![Identifier("b"), Identifier("m")]), + name: Identifier::new("make_coffee"), + parameters: Some(vec![Identifier::new("b"), Identifier::new("m")]), signature: Some(Signature { - requires: Genus::Naked(vec![Forma("Beans"), Forma("Milk")]), - provides: Genus::Single(Forma("Coffee")), + requires: Genus::Naked(vec![Forma::new("Beans"), Forma::new("Milk")]), + provides: Genus::Single(Forma::new("Coffee")), }), elements: Vec::new(), + span: Span::default(), }, ]; @@ -374,37 +386,41 @@ Finally, variables can be assigned for the names of the input parameters: .to_string(), ) } - ParsingError::InvalidParameters(_, _) => { + ParsingError::InvalidParameters(_) => { let examples = vec![ Procedure { - name: Identifier("create_bypass"), - parameters: Some(vec![Identifier("a"), Identifier("b")]), + name: Identifier::new("create_bypass"), + parameters: Some(vec![Identifier::new("a"), Identifier::new("b")]), signature: None, elements: Vec::new(), + span: Span::default(), }, Procedure { - name: Identifier("bulldoze"), - parameters: Some(vec![Identifier("c")]), + name: Identifier::new("bulldoze"), + parameters: Some(vec![Identifier::new("c")]), signature: None, elements: Vec::new(), + span: Span::default(), }, Procedure { - name: Identifier("lawsuit"), + name: Identifier::new("lawsuit"), parameters: None, signature: Some(Signature { - requires: Genus::Single(Forma("Council")), - provides: Genus::List(Forma("Penny")), + requires: Genus::Single(Forma::new("Council")), + provides: Genus::List(Forma::new("Penny")), }), elements: Vec::new(), + span: Span::default(), }, Procedure { - name: Identifier("lawsuit"), - parameters: Some(vec![Identifier("c")]), + name: Identifier::new("lawsuit"), + parameters: Some(vec![Identifier::new("c")]), signature: Some(Signature { - requires: Genus::Single(Forma("Council")), - provides: Genus::List(Forma("Penny")), + requires: Genus::Single(Forma::new("Council")), + provides: Genus::List(Forma::new("Penny")), }), elements: Vec::new(), + span: Span::default(), }, ]; @@ -433,7 +449,7 @@ declarations (and in fact the same): .to_string(), ) } - ParsingError::InvalidSection(_, _) => { + ParsingError::InvalidSection(_) => { // Roman numeral sections don't have AST representation ( "Invalid section heading".to_string(), @@ -453,15 +469,18 @@ author of the Technique. .to_string(), ) } - ParsingError::InvalidInvocation(_, _) => { + ParsingError::InvalidInvocation(_) => { let examples = vec![ Invocation { - target: Target::Local(Identifier("make_coffee")), + target: Target::Local(Identifier::new("make_coffee")), parameters: None, }, Invocation { - target: Target::Local(Identifier("check_vitals")), - parameters: Some(vec![Expression::Variable(Identifier("patient"))]), + target: Target::Local(Identifier::new("check_vitals")), + parameters: Some(vec![Expression::Variable( + Identifier::new("patient"), + Span::default(), + )]), }, ]; @@ -484,21 +503,24 @@ If the procedure takes parameters they can be specified in parenthesis: .to_string(), ) } - ParsingError::InvalidFunction(_, _) => { + ParsingError::InvalidFunction(_) => { let examples = vec![ Function { - target: Identifier("exec"), - parameters: vec![Expression::String(vec![Piece::Text("ls -la")])], + target: Identifier::new("exec"), + parameters: vec![Expression::String( + vec![Piece::Text("ls -la")], + Span::default(), + )], }, Function { - target: Identifier("now"), + target: Identifier::new("now"), parameters: vec![], }, Function { - target: Identifier("calculate"), + target: Identifier::new("calculate"), parameters: vec![ - Expression::Variable(Identifier("a")), - Expression::Variable(Identifier("b")), + Expression::Variable(Identifier::new("a"), Span::default()), + Expression::Variable(Identifier::new("b"), Span::default()), ], }, ]; @@ -523,16 +545,29 @@ expressions as parameters as required: .to_string(), ) } - ParsingError::InvalidCodeBlock(_, _) => { + ParsingError::InvalidCodeBlock(_) => { let examples = vec![ - Expression::Execution(Function { - target: Identifier("exec"), - parameters: vec![Expression::String(vec![Piece::Text("command")])], - }), - Expression::Repeat(Box::new(Expression::Number(Numeric::Integral(5)))), + Expression::Execution( + Function { + target: Identifier::new("exec"), + parameters: vec![Expression::String( + vec![Piece::Text("command")], + Span::default(), + )], + }, + Span::default(), + ), + Expression::Repeat( + Box::new(Expression::Number(Numeric::Integral(5), Span::default())), + Span::default(), + ), Expression::Foreach( - vec![Identifier("patient")], - Box::new(Expression::Variable(Identifier("patients"))), + vec![Identifier::new("patient")], + Box::new(Expression::Variable( + Identifier::new("patients"), + Span::default(), + )), + Span::default(), ), ]; @@ -554,7 +589,7 @@ Inline code blocks are enclosed in braces: .to_string(), ) } - ParsingError::InvalidMultiline(_, _) => ( + ParsingError::InvalidMultiline(_) => ( "Invalid multi-line string".to_string(), r#" Multi-line strings can be written by surrounding the content in triple @@ -584,7 +619,7 @@ it may be used by output templates when rendering the procedure. .trim_ascii() .to_string(), ), - ParsingError::InvalidStep(_, _) => ( + ParsingError::InvalidStep(_) => ( "Invalid step format".to_string(), r#" Steps must start with a number or lower-case letter (in the case of dependent @@ -604,7 +639,7 @@ dash. They can be done in either order, or concurrently: .trim_ascii() .to_string(), ), - ParsingError::InvalidSubstep(_, _) => ( + ParsingError::InvalidSubstep(_) => ( "Invalid substep format".to_string(), r#" Substeps can be nested below top-level dependent steps or top-level parallel @@ -630,22 +665,27 @@ parallel steps, but again this is not compulsory. .trim_ascii() .to_string(), ), - ParsingError::InvalidAttribute(_, _) => { + ParsingError::InvalidAttribute(_) => { let examples = vec![ Scope::AttributeBlock { attributes: vec![ - Attribute::Role(Identifier("president_of_the_galaxy")), - Attribute::Role(Identifier("femme_fatale")), + Attribute::Role( + Identifier::new("president_of_the_galaxy"), + Span::default(), + ), + Attribute::Role(Identifier::new("femme_fatale"), Span::default()), ], subscopes: vec![], + span: Span::default(), }, Scope::AttributeBlock { attributes: vec![ - Attribute::Place(Identifier("milliways")), - Attribute::Role(Identifier("waiter")), - Attribute::Role(Identifier("dish_of_the_day")), + Attribute::Place(Identifier::new("milliways"), Span::default()), + Attribute::Role(Identifier::new("waiter"), Span::default()), + Attribute::Role(Identifier::new("dish_of_the_day"), Span::default()), ], subscopes: vec![], + span: Span::default(), }, ]; @@ -669,15 +709,23 @@ nested underneath a role or place assignment. .to_string(), ) } - ParsingError::InvalidForeach(_, _) => { + ParsingError::InvalidForeach(_) => { let examples = vec![ Expression::Foreach( - vec![Identifier("patient")], - Box::new(Expression::Variable(Identifier("patients"))), + vec![Identifier::new("patient")], + Box::new(Expression::Variable( + Identifier::new("patients"), + Span::default(), + )), + Span::default(), ), Expression::Foreach( - vec![Identifier("name"), Identifier("value")], - Box::new(Expression::Variable(Identifier("data"))), + vec![Identifier::new("name"), Identifier::new("value")], + Box::new(Expression::Variable( + Identifier::new("data"), + Span::default(), + )), + Span::default(), ), ]; @@ -700,34 +748,40 @@ a list of tuples. .to_string(), ) } - ParsingError::InvalidResponse(_, _) => { + ParsingError::InvalidResponse(_) => { let examples = vec![ vec![ Response { value: "Rock", condition: None, + span: Span::default(), }, Response { value: "Paper", condition: None, + span: Span::default(), }, Response { value: "Scissors", condition: None, + span: Span::default(), }, ], vec![Response { value: "Confirmed", condition: None, + span: Span::default(), }], vec![ Response { value: "Yes", condition: Some("but with explanation"), + span: Span::default(), }, Response { value: "No", condition: None, + span: Span::default(), }, ], ]; @@ -762,7 +816,7 @@ By convention the response values are Proper Case. .to_string(), ) } - ParsingError::InvalidIntegral(_, _) => { + ParsingError::InvalidIntegral(_) => { let examples = vec![ Numeric::Integral(42), Numeric::Integral(-123), @@ -790,7 +844,7 @@ Integers cannot contain decimal points or units."#, .to_string(), ) } - ParsingError::InvalidQuantity(_, _) => { + ParsingError::InvalidQuantity(_) => { let examples = vec![ Numeric::Scientific(Quantity { mantissa: Decimal { @@ -855,7 +909,7 @@ a magnitude: .to_string(), ) } - ParsingError::InvalidQuantityDecimal(_, _) => ( + ParsingError::InvalidQuantityDecimal(_) => ( "Invalid number in quantity".to_string(), r#" The numeric part of a quantity may be positive or negative, and may have a @@ -868,7 +922,7 @@ Values less than 1 must have a leading '0' before the decimal."# .trim_ascii() .to_string(), ), - ParsingError::InvalidQuantityUncertainty(_, _) => ( + ParsingError::InvalidQuantityUncertainty(_) => ( "Invalid uncertainty in quantity".to_string(), r#" Uncertainty values must be positive numbers: @@ -879,7 +933,7 @@ You can use '±' or `+/-`, followed by a decimal."# .trim_ascii() .to_string(), ), - ParsingError::InvalidQuantityMagnitude(_, _) => ( + ParsingError::InvalidQuantityMagnitude(_) => ( "Invalid magnitude format".to_string(), r#" The magnitude of a quantity can be expressed in the usual scientific format @@ -894,7 +948,7 @@ The base must be 10, and the exponent must be an integer."# .trim_ascii() .to_string(), ), - ParsingError::InvalidQuantitySymbol(_, _) => { + ParsingError::InvalidQuantitySymbol(_) => { let examples = vec![ Numeric::Scientific(Quantity { mantissa: Decimal { diff --git a/tests/formatting/formatter.rs b/tests/formatting/formatter.rs index 61fd1c8b..15656f00 100644 --- a/tests/formatting/formatter.rs +++ b/tests/formatting/formatter.rs @@ -26,6 +26,7 @@ mod verify { license: Some("MIT"), copyright: None, domain: Some("checklist"), + span: Span::default(), }), body: None, }; @@ -46,13 +47,14 @@ mod verify { source: None, header: None, body: Some(Technique::Procedures(vec![Procedure { - name: Identifier("first"), + name: Identifier::new("first"), parameters: None, signature: Some(Signature { - requires: Genus::Single(Forma("A")), - provides: Genus::Single(Forma("B")), + requires: Genus::Single(Forma::new("A")), + provides: Genus::Single(Forma::new("B")), }), elements: vec![], + span: Span::default(), }])), }; @@ -73,25 +75,32 @@ first : A -> B license: Some("PD"), copyright: Some("2025 The First Procedure Society, Inc"), domain: None, + span: Span::default(), }), body: Some(Technique::Procedures(vec![ Procedure { - name: Identifier("first"), + name: Identifier::new("first"), parameters: None, signature: Some(Signature { - requires: Genus::Single(Forma("A")), - provides: Genus::Single(Forma("B")), + requires: Genus::Single(Forma::new("A")), + provides: Genus::Single(Forma::new("B")), }), elements: vec![], + span: Span::default(), }, Procedure { - name: Identifier("second"), + name: Identifier::new("second"), parameters: None, signature: Some(Signature { - requires: Genus::List(Forma("Thing")), - provides: Genus::Tuple(vec![Forma("Who"), Forma("Where"), Forma("Why")]), + requires: Genus::List(Forma::new("Thing")), + provides: Genus::Tuple(vec![ + Forma::new("Who"), + Forma::new("Where"), + Forma::new("Why"), + ]), }), elements: vec![], + span: Span::default(), }, ])), }; @@ -118,44 +127,59 @@ second : [Thing] -> (Who, Where, Why) source: None, header: None, body: Some(Technique::Procedures(vec![Procedure { - name: Identifier("win_le_tour"), + name: Identifier::new("win_le_tour"), parameters: None, signature: Some(Signature { - requires: Genus::Single(Forma("Bicycle")), - provides: Genus::Single(Forma("YellowJersey")), + requires: Genus::Single(Forma::new("Bicycle")), + provides: Genus::Single(Forma::new("YellowJersey")), }), - elements: vec![Element::Steps(vec![ - Scope::DependentBlock { - ordinal: "1", - description: vec![Paragraph(vec![Descriptive::Text("Eat breakfast.")])], - subscopes: vec![], - }, - Scope::DependentBlock { - ordinal: "2", - description: vec![Paragraph(vec![Descriptive::Text("Win a stage:")])], - subscopes: vec![ - Scope::DependentBlock { - ordinal: "a", - description: vec![Paragraph(vec![Descriptive::Text( - "Ride really fast, then", - )])], - subscopes: vec![], - }, - Scope::DependentBlock { - ordinal: "b", - description: vec![Paragraph(vec![Descriptive::Text( - "Win the sprint.", - )])], - subscopes: vec![], - }, - ], - }, - Scope::DependentBlock { - ordinal: "3", - description: vec![Paragraph(vec![Descriptive::Text("Eat dinner.")])], - subscopes: vec![], - }, - ])], + elements: vec![Element::Steps( + vec![ + Scope::DependentBlock { + ordinal: "1", + description: vec![Paragraph::new(vec![Descriptive::Text( + "Eat breakfast.", + )])], + subscopes: vec![], + span: Span::default(), + }, + Scope::DependentBlock { + ordinal: "2", + description: vec![Paragraph::new(vec![Descriptive::Text( + "Win a stage:", + )])], + subscopes: vec![ + Scope::DependentBlock { + ordinal: "a", + description: vec![Paragraph::new(vec![Descriptive::Text( + "Ride really fast, then", + )])], + subscopes: vec![], + span: Span::default(), + }, + Scope::DependentBlock { + ordinal: "b", + description: vec![Paragraph::new(vec![Descriptive::Text( + "Win the sprint.", + )])], + subscopes: vec![], + span: Span::default(), + }, + ], + span: Span::default(), + }, + Scope::DependentBlock { + ordinal: "3", + description: vec![Paragraph::new(vec![Descriptive::Text( + "Eat dinner.", + )])], + subscopes: vec![], + span: Span::default(), + }, + ], + Span::default(), + )], + span: Span::default(), }])), }; @@ -182,13 +206,24 @@ win_le_tour : Bicycle -> YellowJersey source: None, header: None, body: Some(Technique::Procedures(vec![Procedure { - name: Identifier("vibe_coding"), + name: Identifier::new("vibe_coding"), parameters: None, signature: None, - elements: vec![Element::CodeBlock(vec![Expression::Execution(Function { - target: Identifier("exec"), - parameters: vec![Expression::Multiline(Some("bash"), vec!["rm -rf /"])], - })])], + elements: vec![Element::CodeBlock( + vec![Expression::Execution( + Function { + target: Identifier::new("exec"), + parameters: vec![Expression::Multiline( + Some("bash"), + vec!["rm -rf /"], + Span::default(), + )], + }, + Span::default(), + )], + Span::default(), + )], + span: Span::default(), }])), }; @@ -216,28 +251,40 @@ vibe_coding : source: None, header: None, body: Some(Technique::Procedures(vec![Procedure { - name: Identifier("action"), + name: Identifier::new("action"), parameters: None, signature: None, elements: vec![ - Element::Description(vec![Paragraph(vec![Descriptive::Text( - "We must take action!", - )])]), - Element::Steps(vec![Scope::DependentBlock { - ordinal: "1", - description: vec![Paragraph(vec![ - Descriptive::Text("To take the action, we must:"), - Descriptive::CodeInline(Expression::Execution(Function { - target: Identifier("exec"), - parameters: vec![Expression::Multiline( - Some("bash"), - vec!["rm -rf /"], - )], - })), - ])], - subscopes: vec![], - }]), + Element::Description( + vec![Paragraph::new(vec![Descriptive::Text( + "We must take action!", + )])], + Span::default(), + ), + Element::Steps( + vec![Scope::DependentBlock { + ordinal: "1", + description: vec![Paragraph::new(vec![ + Descriptive::Text("To take the action, we must:"), + Descriptive::CodeInline(Expression::Execution( + Function { + target: Identifier::new("exec"), + parameters: vec![Expression::Multiline( + Some("bash"), + vec!["rm -rf /"], + Span::default(), + )], + }, + Span::default(), + )), + ])], + subscopes: vec![], + span: Span::default(), + }], + Span::default(), + ), ], + span: Span::default(), }])), }; @@ -266,39 +313,61 @@ We must take action! source: None, header: None, body: Some(Technique::Procedures(vec![Procedure { - name: Identifier("journal"), + name: Identifier::new("journal"), parameters: None, signature: None, elements: vec![ - Element::Description(vec![Paragraph(vec![Descriptive::Text( - "Record everything, with timestamps.", - )])]), - Element::Steps(vec![Scope::ParallelBlock { - bullet: '-', - description: vec![Paragraph(vec![Descriptive::Text( - "Record event as it happens", + Element::Description( + vec![Paragraph::new(vec![Descriptive::Text( + "Record everything, with timestamps.", )])], - subscopes: vec![Scope::AttributeBlock { - attributes: vec![Attribute::Role(Identifier("journalist"))], - subscopes: vec![Scope::CodeBlock { - expressions: vec![Expression::Tablet(vec![ - Pair { - label: "timestamp", - value: Expression::Execution(Function { - target: Identifier("now"), - parameters: vec![], - }), - }, - Pair { - label: "message", - value: Expression::Variable(Identifier("msg")), - }, - ])], - subscopes: vec![], + Span::default(), + ), + Element::Steps( + vec![Scope::ParallelBlock { + bullet: '-', + description: vec![Paragraph::new(vec![Descriptive::Text( + "Record event as it happens", + )])], + subscopes: vec![Scope::AttributeBlock { + attributes: vec![Attribute::Role( + Identifier::new("journalist"), + Span::default(), + )], + subscopes: vec![Scope::CodeBlock { + expressions: vec![Expression::Tablet( + vec![ + Pair { + label: "timestamp", + value: Expression::Execution( + Function { + target: Identifier::new("now"), + parameters: vec![], + }, + Span::default(), + ), + }, + Pair { + label: "message", + value: Expression::Variable( + Identifier::new("msg"), + Span::default(), + ), + }, + ], + Span::default(), + )], + subscopes: vec![], + span: Span::default(), + }], + span: Span::default(), }], + span: Span::default(), }], - }]), + Span::default(), + ), ], + span: Span::default(), }])), }; @@ -330,66 +399,85 @@ Record everything, with timestamps. source: None, header: None, body: Some(Technique::Procedures(vec![Procedure { - name: Identifier("before_leaving"), + name: Identifier::new("before_leaving"), parameters: None, signature: None, elements: vec![ - Element::Title("Before patient leaves operating room"), - Element::Steps(vec![Scope::DependentBlock { - ordinal: "1", - description: vec![Paragraph(vec![Descriptive::Text("Verbally confirm:")])], - subscopes: vec![ - Scope::ParallelBlock { - bullet: '-', - description: vec![Paragraph(vec![Descriptive::Text( - "The name of the surgical procedure(s).", - )])], - subscopes: vec![], - }, - Scope::ParallelBlock { - bullet: '-', - description: vec![Paragraph(vec![Descriptive::Text( - "Completion of instrument, sponge, and needle counts.", - )])], - subscopes: vec![], - }, - Scope::ParallelBlock { - bullet: '-', - description: vec![Paragraph(vec![Descriptive::Text( - "Specimen labelling", - )])], - subscopes: vec![Scope::CodeBlock { - expressions: vec![Expression::Foreach( - vec![Identifier("specimen")], - Box::new(Expression::Variable(Identifier("specimens"))), - )], - subscopes: vec![Scope::AttributeBlock { - attributes: vec![Attribute::Role(Identifier( - "nursing_team", - ))], - subscopes: vec![Scope::DependentBlock { - ordinal: "a", - description: vec![Paragraph(vec![ + Element::Title("Before patient leaves operating room", Span::default()), + Element::Steps( + vec![Scope::DependentBlock { + ordinal: "1", + description: vec![Paragraph::new(vec![Descriptive::Text( + "Verbally confirm:", + )])], + subscopes: vec![ + Scope::ParallelBlock { + bullet: '-', + description: vec![Paragraph::new(vec![Descriptive::Text( + "The name of the surgical procedure(s).", + )])], + subscopes: vec![], + span: Span::default(), + }, + Scope::ParallelBlock { + bullet: '-', + description: vec![Paragraph::new(vec![Descriptive::Text( + "Completion of instrument, sponge, and needle counts.", + )])], + subscopes: vec![], + span: Span::default(), + }, + Scope::ParallelBlock { + bullet: '-', + description: vec![Paragraph::new(vec![Descriptive::Text( + "Specimen labelling", + )])], + subscopes: vec![Scope::CodeBlock { + expressions: vec![Expression::Foreach( + vec![Identifier::new("specimen")], + Box::new(Expression::Variable( + Identifier::new("specimens"), + Span::default(), + )), + Span::default(), + )], + subscopes: vec![Scope::AttributeBlock { + attributes: vec![Attribute::Role( + Identifier::new("nursing_team"), + Span::default(), + )], + subscopes: vec![Scope::DependentBlock { + ordinal: "a", + description: vec![Paragraph::new(vec![ Descriptive::Text( "Read specimen labels aloud, including patient", ), Descriptive::Text("name."), ])], - subscopes: vec![], + subscopes: vec![], + span: Span::default(), + }], + span: Span::default(), }], + span: Span::default(), }], - }], - }, - Scope::ParallelBlock { - bullet: '-', - description: vec![Paragraph(vec![Descriptive::Text( - "Whether there are any equipment problems to be addressed.", - )])], - subscopes: vec![], - }, - ], - }]), + span: Span::default(), + }, + Scope::ParallelBlock { + bullet: '-', + description: vec![Paragraph::new(vec![Descriptive::Text( + "Whether there are any equipment problems to be addressed.", + )])], + subscopes: vec![], + span: Span::default(), + }, + ], + span: Span::default(), + }], + Span::default(), + ), ], + span: Span::default(), }])), }; let result = format_with_renderer(&document, 60); @@ -424,26 +512,33 @@ before_leaving : source: None, header: None, body: Some(Technique::Procedures(vec![Procedure { - name: Identifier("main_procedure"), + name: Identifier::new("main_procedure"), parameters: None, signature: None, - elements: vec![Element::Steps(vec![ - Scope::SectionChunk { - numeral: "I", - title: Some(Paragraph(vec![Descriptive::Text("First Section")])), - body: Technique::Procedures(vec![]), - }, - Scope::SectionChunk { - numeral: "II", - title: Some(Paragraph(vec![Descriptive::Text("Second Section")])), - body: Technique::Procedures(vec![]), - }, - Scope::SectionChunk { - numeral: "III", - title: None, - body: Technique::Procedures(vec![]), - }, - ])], + elements: vec![Element::Steps( + vec![ + Scope::SectionChunk { + numeral: "I", + title: Some(Paragraph::new(vec![Descriptive::Text("First Section")])), + body: Technique::Procedures(vec![]), + span: Span::default(), + }, + Scope::SectionChunk { + numeral: "II", + title: Some(Paragraph::new(vec![Descriptive::Text("Second Section")])), + body: Technique::Procedures(vec![]), + span: Span::default(), + }, + Scope::SectionChunk { + numeral: "III", + title: None, + body: Technique::Procedures(vec![]), + span: Span::default(), + }, + ], + Span::default(), + )], + span: Span::default(), }])), }; @@ -470,45 +565,57 @@ III. source: None, header: None, body: Some(Technique::Procedures(vec![Procedure { - name: Identifier("test_procedure"), + name: Identifier::new("test_procedure"), parameters: None, signature: None, - elements: vec![Element::Steps(vec![ - Scope::DependentBlock { - ordinal: "1", - description: vec![Paragraph(vec![Descriptive::Text("Main step")])], - subscopes: vec![Scope::DependentBlock { - ordinal: "a", - description: vec![Paragraph(vec![Descriptive::Text( - "Substep with response", + elements: vec![Element::Steps( + vec![ + Scope::DependentBlock { + ordinal: "1", + description: vec![Paragraph::new(vec![Descriptive::Text("Main step")])], + subscopes: vec![Scope::DependentBlock { + ordinal: "a", + description: vec![Paragraph::new(vec![Descriptive::Text( + "Substep with response", + )])], + subscopes: vec![Scope::ResponseBlock { + responses: vec![ + Response { + value: "Yes", + condition: None, + span: Span::default(), + }, + Response { + value: "No", + condition: None, + span: Span::default(), + }, + ], + span: Span::default(), + }], + span: Span::default(), + }], + span: Span::default(), + }, + Scope::DependentBlock { + ordinal: "2", + description: vec![Paragraph::new(vec![Descriptive::Text( + "Simple step with response", )])], subscopes: vec![Scope::ResponseBlock { - responses: vec![ - Response { - value: "Yes", - condition: None, - }, - Response { - value: "No", - condition: None, - }, - ], - }], - }], - }, - Scope::DependentBlock { - ordinal: "2", - description: vec![Paragraph(vec![Descriptive::Text( - "Simple step with response", - )])], - subscopes: vec![Scope::ResponseBlock { - responses: vec![Response { - value: "Confirmed", - condition: None, + responses: vec![Response { + value: "Confirmed", + condition: None, + span: Span::default(), + }], + span: Span::default(), }], - }], - }, - ])], + span: Span::default(), + }, + ], + Span::default(), + )], + span: Span::default(), }])), }; diff --git a/tests/samples/KnownSpanLengths.tq b/tests/samples/KnownSpanLengths.tq new file mode 100644 index 00000000..1990c0f4 --- /dev/null +++ b/tests/samples/KnownSpanLengths.tq @@ -0,0 +1,14 @@ +% technique v1 +! MIT; © ACME +& checklist + +greet(name) : Person -> Greeting + +# Hello World + +Say hello to someone. + + 1. Look at the person. + @greeter + 2. Say { exec("hello") } to them. + 'Done' | 'Skipped' but too shy | 'Elvis Has Left The Building'