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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions src/parsing/checks/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
93 changes: 63 additions & 30 deletions src/parsing/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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();
}
Expand Down Expand Up @@ -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('~')
Expand Down Expand Up @@ -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<Item = (usize, char)> + '_ {
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 {
Expand Down
6 changes: 6 additions & 0 deletions tests/golden/runner/Century.tq
Original file line number Diff line number Diff line change
Expand Up @@ -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
}