diff --git a/src/parsing/checks/parser.rs b/src/parsing/checks/parser.rs index f92b324..dd13654 100644 --- a/src/parsing/checks/parser.rs +++ b/src/parsing/checks/parser.rs @@ -2306,6 +2306,56 @@ fn binding_followed_by_use_in_same_code_block() { ); } +#[test] +fn binding_followed_by_use_separated_by_newline_only() { + let mut input = Parser::new(); + input.initialize("{\n now() ~ t\n t\n}"); + + let result = input.read_code_block(); + assert_eq!( + result, + Ok(vec![ + Expression::Binding( + Box::new(Expression::Execution( + Function { + target: Identifier::new("now"), + parameters: vec![] + }, + Span::default() + )), + vec![Identifier::new("t")], + Span::default() + ), + Expression::Variable(Identifier::new("t"), Span::default()) + ]) + ); +} + +#[test] +fn statement_before_later_binding_not_mistaken_for_binding() { + let mut input = Parser::new(); + input.initialize("{\n now()\n x ~ t\n}"); + + let result = input.read_code_block(); + assert_eq!( + result, + Ok(vec![ + Expression::Execution( + Function { + target: Identifier::new("now"), + parameters: vec![] + }, + Span::default() + ), + Expression::Binding( + Box::new(Expression::Variable(Identifier::new("x"), Span::default())), + vec![Identifier::new("t")], + Span::default() + ) + ]) + ); +} + #[test] fn test_repeat_expression() { let mut input = Parser::new(); diff --git a/src/parsing/parser.rs b/src/parsing/parser.rs index b406541..eb896c4 100644 --- a/src/parsing/parser.rs +++ b/src/parsing/parser.rs @@ -647,10 +647,6 @@ impl<'i> Parser<'i> { let mut results = Vec::new(); let mut start = 0; - let mut depth = 0i32; - let mut in_string = false; - let mut in_multiline = false; - let mut backticks = 0u8; let mut cut = |outer: &mut Parser<'i>, chunk: &'i str| -> Result<(), ParsingError> { let trimmed = chunk.trim_ascii(); @@ -666,28 +662,13 @@ impl<'i> Parser<'i> { Ok(()) }; - for (i, c) in content.char_indices() { - if c == '`' { - backticks += 1; - if backticks == 3 { - in_multiline = !in_multiline; - backticks = 0; - } - continue; - } - backticks = 0; - + for (i, c) in top_level_chars(content) { match c { - _ if in_multiline => {} - '"' => in_string = !in_string, - _ if in_string => {} - '(' | '[' => depth += 1, - ')' | ']' => depth -= 1, - ',' if depth == 0 => { + ',' => { cut(self, &content[start..i])?; start = i + c.len_utf8(); } - '\n' if depth == 0 && allow_newline => { + '\n' if allow_newline => { cut(self, &content[start..i])?; start = i + c.len_utf8(); } @@ -1518,9 +1499,11 @@ impl<'i> Parser<'i> { .source .trim_ascii_start(); - if is_binding(content) { + let statement = &content[..locate_statement_end(content)]; + + if is_binding(statement) { self.read_binding_expression() - } else if malformed_binding_pattern(content) { + } else if malformed_binding_pattern(statement) { if let Some(tilde_pos) = self .source .find('~') @@ -3319,17 +3302,67 @@ fn is_function(content: &str) -> bool { re.is_match(content) } -fn is_binding(content: &str) -> bool { - let re = - regex!(r"~\s+([a-z][a-z0-9_]*|\([a-z][a-z0-9_]*(?:\s*,\s*[a-z][a-z0-9_]*)*\))\s*(;|$)"); +// Iterate the `(offset, char)` pairs of `content` that sit at the top level — +// not nested inside `()`/`[]`, a `"..."` string, or a ``` multiline fence. +// Shared by take_elements() and locate_statement_end() so the two don't drift. +fn top_level_chars(content: &str) -> impl Iterator + '_ { + let mut depth = 0i32; + let mut in_string = false; + let mut in_multiline = false; + let mut backticks = 0u8; - re.is_match(content) + content + .char_indices() + .filter_map(move |(i, c)| { + if c == '`' { + backticks += 1; + if backticks == 3 { + in_multiline = !in_multiline; + backticks = 0; + } + return None; + } + backticks = 0; + + match c { + _ if in_multiline => None, + '"' => { + in_string = !in_string; + None + } + _ if in_string => None, + '(' | '[' => { + depth += 1; + None + } + ')' | ']' => { + depth -= 1; + None + } + _ if depth == 0 => Some((i, c)), + _ => None, + } + }) +} + +// Bound the current statement so is_binding()'s trailing `$` can't reach past it into a later one. +fn locate_statement_end(content: &str) -> usize { + top_level_chars(content) + .find(|&(_, c)| c == ';' || c == '\n') + .map(|(i, _)| i) + .unwrap_or(content.len()) } -fn malformed_binding_pattern(content: &str) -> bool { +fn is_binding(statement: &str) -> bool { + let re = regex!(r"~\s+([a-z][a-z0-9_]*|\([a-z][a-z0-9_]*(?:\s*,\s*[a-z][a-z0-9_]*)*\))\s*$"); + + re.is_match(statement) +} + +fn malformed_binding_pattern(statement: &str) -> bool { // Detect ~ identifier, identifier (missing parentheses) let re = regex!(r"~\s+[a-z][a-z0-9_]*\s*,\s*[a-z]"); - re.is_match(content) + re.is_match(statement) } fn is_step_dependent(content: &str) -> bool { diff --git a/tests/golden/runner/Century.tq b/tests/golden/runner/Century.tq index fc79b06..4931545 100644 --- a/tests/golden/runner/Century.tq +++ b/tests/golden/runner/Century.tq @@ -5,3 +5,9 @@ twenty_first_century : Unfortunately this test will fail in the Twenty-Second century. 1. This should output "20" { exec("date -u +%C") ~ t ; t } + +happiness : +{ + "Small green pieces of paper" ~ money + money +}