use super::*;
fn stmt_block_of(p: &Parse) -> ast::StmtBlock {
let file = ast::SourceFile::cast(p.syntax()).expect("SOURCE_FILE");
let var_decl: ast::VarDecl = find_child(file.syntax()).expect("var decl");
let value = var_decl.value().expect("initializer node");
ast::StmtBlock::cast(value).expect("STMT_BLOCK")
}
#[test]
fn empty_block_has_no_tail_and_no_errors() {
let p = assert_lossless("var x = { }\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let block = stmt_block_of(&p);
assert_eq!(block.items().count(), 0);
assert!(block.tail().is_none());
}
#[test]
fn block_is_reachable_as_a_call_argument() {
let p = assert_lossless("var x = foo({ 1 })\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::STMT_BLOCK));
}
#[test]
fn let_stmt_with_initializer() {
let p = assert_lossless("var x = { let y = 1; y }\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let block = stmt_block_of(&p);
let items: Vec<_> = block.items().collect();
assert_eq!(items.len(), 2, "LET_STMT + tail");
assert_eq!(items[0].kind(), SyntaxKind::LET_STMT);
let let_stmt = ast::LetStmt::cast(items[0].clone()).expect("LET_STMT");
assert_eq!(
let_stmt.name_token().map(|t| t.text().to_string()),
Some("y".to_string())
);
assert_eq!(
let_stmt.value().map(|n| n.kind()),
Some(SyntaxKind::INTEGER_LIT)
);
let tail = block.tail().expect("tail");
assert_eq!(tail.kind(), SyntaxKind::PATH_EXPR);
}
#[test]
fn let_stmt_without_initializer() {
let p = assert_lossless("var x = { let y; y }\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let block = stmt_block_of(&p);
let let_stmt: ast::LetStmt = find_child(block.syntax()).expect("LET_STMT");
assert_eq!(
let_stmt.name_token().map(|t| t.text().to_string()),
Some("y".to_string())
);
assert!(let_stmt.value().is_none());
}
#[test]
fn let_stmt_with_type_annotation_and_initializer() {
let p = assert_lossless("var x = { let y: int = 1; y }\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let block = stmt_block_of(&p);
let let_stmt: ast::LetStmt = find_child(block.syntax()).expect("LET_STMT");
let annotation = let_stmt.type_annotation().expect("`: int` annotation");
let te = annotation.type_expr().expect("type expr");
let Some(ast::TypeExprKind::Name(n)) = te.kind() else {
unreachable!("expected a nominal type, tree: {:#?}", te.syntax())
};
assert_eq!(n.name(), Some("int".to_string()));
assert_eq!(
let_stmt.value().map(|n| n.kind()),
Some(SyntaxKind::INTEGER_LIT)
);
}
#[test]
fn let_stmt_with_type_annotation_and_no_initializer() {
let p = assert_lossless("var x = { let y: string; y }\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let block = stmt_block_of(&p);
let let_stmt: ast::LetStmt = find_child(block.syntax()).expect("LET_STMT");
assert!(let_stmt.type_annotation().is_some());
assert!(
let_stmt.value().is_none(),
"the annotation is not an initializer"
);
}
#[test]
fn let_stmt_square_bracket_after_type_name_gets_the_unified_message_first() {
let p = parse("fn f() { let x: Option[int] = none; }\n");
assert_eq!(
p.errors().first().map(|e| e.message.as_str()),
Some("expected `<` or end of type name, found L_BRACKET"),
"errors: {:?}",
p.errors()
);
}
#[test]
fn assign_stmt_simple_place() {
let p = assert_lossless("var x = { y = 1; y }\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let block = stmt_block_of(&p);
let assign: ast::AssignStmt = find_child(block.syntax()).expect("ASSIGN_STMT");
let place = assign.place().expect("place path");
assert_eq!(
place
.segments()
.map(|t| t.text().to_string())
.collect::<Vec<_>>(),
vec!["y".to_string()]
);
assert_eq!(
assign.value().map(|n| n.kind()),
Some(SyntaxKind::INTEGER_LIT)
);
}
#[test]
fn assign_stmt_field_rmw_path() {
let p = assert_lossless("var x = { player.hp = 10; player }\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let block = stmt_block_of(&p);
let assign: ast::AssignStmt = find_child(block.syntax()).expect("ASSIGN_STMT");
let place = assign.place().expect("place path");
assert_eq!(
place
.segments()
.map(|t| t.text().to_string())
.collect::<Vec<_>>(),
vec!["player".to_string(), "hp".to_string()]
);
}
#[test]
fn assign_stmt_rhs_is_a_full_expression() {
let p = assert_lossless("var x = { y = 1 + 2 * 3; y }\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let block = stmt_block_of(&p);
let assign: ast::AssignStmt = find_child(block.syntax()).expect("ASSIGN_STMT");
assert_eq!(
assign.value().map(|n| n.kind()),
Some(SyntaxKind::INFIX_EXPR)
);
}
#[test]
fn double_equals_is_not_an_assignment() {
let p = assert_lossless("var x = { a == b; 1 }\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let block = stmt_block_of(&p);
assert!(!has_node_kind(block.syntax(), SyntaxKind::ASSIGN_STMT));
let expr_stmt: ast::ExprStmt = find_child(block.syntax()).expect("EXPR_STMT");
assert_eq!(
expr_stmt.expr().map(|n| n.kind()),
Some(SyntaxKind::INFIX_EXPR)
);
}
#[test]
fn expr_stmt_call() {
let p = assert_lossless("var x = { foo(); 1 }\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let block = stmt_block_of(&p);
let expr_stmt: ast::ExprStmt = find_child(block.syntax()).expect("EXPR_STMT");
assert_eq!(
expr_stmt.expr().map(|n| n.kind()),
Some(SyntaxKind::CALL_EXPR)
);
}
#[test]
fn trailing_semicolon_makes_the_last_expression_a_statement_not_a_tail() {
let p = assert_lossless("var x = { 1 + 2; }\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let block = stmt_block_of(&p);
assert!(block.tail().is_none());
let items: Vec<_> = block.items().collect();
assert_eq!(items.len(), 1);
assert_eq!(items[0].kind(), SyntaxKind::EXPR_STMT);
}
#[test]
fn missing_trailing_semicolon_makes_the_last_expression_the_tail() {
let p = assert_lossless("var x = { 1 + 2 }\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let block = stmt_block_of(&p);
let tail = block.tail().expect("tail");
assert_eq!(tail.kind(), SyntaxKind::INFIX_EXPR);
let items: Vec<_> = block.items().collect();
assert_eq!(items.len(), 1, "the tail is the block's only child here");
}
#[test]
fn single_bare_ident_tail() {
let p = assert_lossless("var x = { y }\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let block = stmt_block_of(&p);
assert_eq!(block.tail().map(|n| n.kind()), Some(SyntaxKind::PATH_EXPR));
}
#[test]
fn dispatcher_let_then_assign_then_expr_stmt_then_tail() {
let p = assert_lossless("var x = { let a = 1; a = 2; foo(a); a }\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let block = stmt_block_of(&p);
let items: Vec<_> = block.items().collect();
let kinds: Vec<_> = items.iter().map(rowan::SyntaxNode::kind).collect();
assert_eq!(
kinds,
vec![
SyntaxKind::LET_STMT,
SyntaxKind::ASSIGN_STMT,
SyntaxKind::EXPR_STMT,
SyntaxKind::PATH_EXPR,
]
);
let tail = block.tail().expect("tail");
assert_eq!(tail.kind(), SyntaxKind::PATH_EXPR);
}
#[test]
fn nested_statement_block_as_a_let_initializer() {
let p = assert_lossless("var x = { let a = { let b = 1; b }; a }\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::STMT_BLOCK), 2);
let block = stmt_block_of(&p);
let let_stmt: ast::LetStmt = find_child(block.syntax()).expect("LET_STMT");
assert_eq!(
let_stmt.value().map(|n| n.kind()),
Some(SyntaxKind::STMT_BLOCK)
);
}
#[test]
fn error_let_missing_name_does_not_panic() {
let src = "var x = { let = 1; 1 }\n";
let p = parse(src);
assert_eq!(src, p.syntax().text().to_string(), "lossless round-trip");
assert!(!p.errors().is_empty());
}
#[test]
fn error_assign_missing_rhs_does_not_panic() {
let src = "var x = { a = ; 1 }\n";
let p = parse(src);
assert_eq!(src, p.syntax().text().to_string(), "lossless round-trip");
assert!(!p.errors().is_empty());
}
#[test]
fn error_garbage_token_inside_block_recovers() {
let src = "var x = { @ 1 }\n";
let p = parse(src);
assert_eq!(src, p.syntax().text().to_string(), "lossless round-trip");
assert!(!p.errors().is_empty());
assert!(has_node_kind(&p.syntax(), SyntaxKind::ERROR));
}
#[test]
fn error_missing_semicolon_after_let_still_finds_a_tail() {
let src = "var x = { let a = 1 a }\n";
let p = parse(src);
assert_eq!(src, p.syntax().text().to_string(), "lossless round-trip");
assert!(!p.errors().is_empty());
let block = stmt_block_of(&p);
assert!(block.tail().is_some());
}
#[test]
fn error_unclosed_block_at_eof_does_not_panic() {
let src = "var x = { let a = 1;";
let p = parse(src);
assert_eq!(src, p.syntax().text().to_string(), "lossless round-trip");
assert!(!p.errors().is_empty());
}
#[test]
fn adversarial_deeply_nested_blocks_does_not_panic() {
let src = format!("var x = {}1{}\n", "{".repeat(300), "}".repeat(300));
let p = parse(&src);
assert_eq!(src, p.syntax().text().to_string(), "lossless round-trip");
assert!(
!p.errors().is_empty(),
"expected a max-nesting-depth error, not silent success or a panic"
);
}
#[test]
fn adversarial_many_statements_does_not_panic() {
use std::fmt::Write as _;
let mut src = "var x = { ".to_string();
for i in 0..500 {
let _ = writeln!(src, "let v{i} = {i};");
}
src.push_str("0 }\n");
let p = parse(&src);
assert_eq!(src, p.syntax().text().to_string(), "lossless round-trip");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let block = stmt_block_of(&p);
assert_eq!(block.items().count(), 501, "500 LET_STMTs + the `0` tail");
assert_eq!(
block.tail().map(|n| n.kind()),
Some(SyntaxKind::INTEGER_LIT)
);
}
#[test]
fn if_stmt_no_else() {
let p = assert_lossless("var x = { if a { 1; } 2 }\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let block = stmt_block_of(&p);
let if_stmt: ast::IfStmt = find_child(block.syntax()).expect("IF_STMT");
assert_eq!(
if_stmt.condition().map(|n| n.kind()),
Some(SyntaxKind::PATH_EXPR)
);
assert!(if_stmt.body().is_some());
assert!(if_stmt.else_clause().is_none());
}
#[test]
fn if_else_stmt() {
let p = assert_lossless("var x = { if a { 1; } else { 2; } 3 }\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let block = stmt_block_of(&p);
let if_stmt: ast::IfStmt = find_child(block.syntax()).expect("IF_STMT");
let else_clause = if_stmt.else_clause().expect("ELSE_CLAUSE");
assert!(else_clause.body().is_some());
assert!(else_clause.if_stmt().is_none());
}
#[test]
fn else_if_chain_is_a_nested_if_stmt_with_no_extra_stmt_block() {
let p = assert_lossless("var x = { if a { 1; } else if b { 2; } 3 }\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let block = stmt_block_of(&p);
let if_stmt: ast::IfStmt = find_child(block.syntax()).expect("IF_STMT");
let else_clause = if_stmt.else_clause().expect("ELSE_CLAUSE");
let nested = else_clause.if_stmt().expect("chained IF_STMT");
assert_eq!(
nested.condition().map(|n| n.kind()),
Some(SyntaxKind::PATH_EXPR)
);
assert!(else_clause.body().is_none());
}
#[test]
fn while_stmt_shape() {
let p = assert_lossless("var x = { while a { b = b + 1; } 0 }\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let block = stmt_block_of(&p);
let while_stmt: ast::WhileStmt = find_child(block.syntax()).expect("WHILE_STMT");
assert_eq!(
while_stmt.condition().map(|n| n.kind()),
Some(SyntaxKind::PATH_EXPR)
);
let body = while_stmt.body().expect("body");
assert!(has_node_kind(body.syntax(), SyntaxKind::ASSIGN_STMT));
}
#[test]
fn for_stmt_shape() {
let p = assert_lossless("var x = { for item in items { foo(item); } 0 }\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let block = stmt_block_of(&p);
let for_stmt: ast::ForStmt = find_child(block.syntax()).expect("FOR_STMT");
assert_eq!(
for_stmt.name_token().map(|t| t.text().to_string()),
Some("item".to_string())
);
assert_eq!(
for_stmt.iterable().map(|n| n.kind()),
Some(SyntaxKind::PATH_EXPR)
);
let body = for_stmt.body().expect("body");
assert!(has_node_kind(body.syntax(), SyntaxKind::EXPR_STMT));
}
#[test]
fn for_stmt_two_binding_shape() {
let p = assert_lossless("var x = { for k, v in m { foo(k, v); } 0 }\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let block = stmt_block_of(&p);
let for_stmt: ast::ForStmt = find_child(block.syntax()).expect("FOR_STMT");
assert_eq!(
for_stmt.name_token().map(|t| t.text().to_string()),
Some("k".to_string())
);
assert_eq!(
for_stmt.val_name_token().map(|t| t.text().to_string()),
Some("v".to_string())
);
assert_eq!(
for_stmt.iterable().map(|n| n.kind()),
Some(SyntaxKind::PATH_EXPR)
);
}
#[test]
fn for_stmt_single_binding_has_no_val_name() {
let p = assert_lossless("var x = { for item in items { foo(item); } 0 }\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let block = stmt_block_of(&p);
let for_stmt: ast::ForStmt = find_child(block.syntax()).expect("FOR_STMT");
assert!(for_stmt.val_name_token().is_none());
}
#[test]
fn until_stmt_shape() {
let p = assert_lossless("var x = { until door_open; 0 }\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let block = stmt_block_of(&p);
let until_stmt: ast::UntilStmt = find_child(block.syntax()).expect("UNTIL_STMT");
assert_eq!(
until_stmt.condition().map(|n| n.kind()),
Some(SyntaxKind::PATH_EXPR)
);
}
#[test]
fn await_is_not_a_native_keyword() {
let p = assert_lossless("var x = { let await = 1; await }\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(!has_node_kind(&p.syntax(), SyntaxKind::UNTIL_STMT));
}
#[test]
fn if_stmt_as_the_last_item_is_not_mistaken_for_a_tail() {
let p = assert_lossless("var x = { if a { 1; } }\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let block = stmt_block_of(&p);
assert!(block.tail().is_none());
let items: Vec<_> = block.items().collect();
assert_eq!(items.len(), 1);
assert_eq!(items[0].kind(), SyntaxKind::IF_STMT);
}
#[test]
fn prose_line_as_the_last_item_is_not_mistaken_for_a_tail() {
let p = assert_lossless("var x = { > hi }\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let block = stmt_block_of(&p);
assert!(block.tail().is_none());
let items: Vec<_> = block.items().collect();
assert_eq!(items.len(), 1);
assert_eq!(items[0].kind(), SyntaxKind::PROSE_LINE);
}
#[test]
fn control_flow_bodies_nest_and_recurse() {
let p = assert_lossless(
"var x = { for i in xs { while a { if b { c = 1; } else { c = 2; } } } 0 }\n",
);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::FOR_STMT));
assert!(has_node_kind(&p.syntax(), SyntaxKind::WHILE_STMT));
assert!(has_node_kind(&p.syntax(), SyntaxKind::IF_STMT));
assert!(has_node_kind(&p.syntax(), SyntaxKind::ELSE_CLAUSE));
}
#[test]
fn error_if_missing_condition_does_not_panic() {
let src = "var x = { if { 1; } 0 }\n";
let p = parse(src);
assert_eq!(src, p.syntax().text().to_string(), "lossless round-trip");
assert!(!p.errors().is_empty());
}
#[test]
fn error_for_missing_in_does_not_panic() {
let src = "var x = { for item items { 0; } 0 }\n";
let p = parse(src);
assert_eq!(src, p.syntax().text().to_string(), "lossless round-trip");
assert!(!p.errors().is_empty());
}
#[test]
fn error_until_missing_semicolon_does_not_panic() {
let src = "var x = { until a 0 }\n";
let p = parse(src);
assert_eq!(src, p.syntax().text().to_string(), "lossless round-trip");
assert!(!p.errors().is_empty());
}
#[test]
fn if_stmt_as_binding_is_a_sibling_of_the_condition() {
let p = assert_lossless("var x = { if find(s, \"a\") as i { i; } 0 }\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let block = stmt_block_of(&p);
let if_stmt: ast::IfStmt = find_child(block.syntax()).expect("IF_STMT");
assert_eq!(
if_stmt.condition().map(|n| n.kind()),
Some(SyntaxKind::CALL_EXPR)
);
let binding = if_stmt.as_binding().expect("AS_BINDING");
assert_eq!(
binding.name_token().map(|t| t.text().to_string()),
Some("i".to_string())
);
assert!(if_stmt.body().is_some());
}
#[test]
fn if_stmt_without_as_has_no_binding() {
let p = assert_lossless("var x = { if a { 1; } 0 }\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let block = stmt_block_of(&p);
let if_stmt: ast::IfStmt = find_child(block.syntax()).expect("IF_STMT");
assert!(if_stmt.as_binding().is_none());
}
#[test]
fn while_stmt_as_binding_parses() {
let p = assert_lossless("var x = { while pop(q) as item { consume(item); } 0 }\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let block = stmt_block_of(&p);
let while_stmt: ast::WhileStmt = find_child(block.syntax()).expect("WHILE_STMT");
assert_eq!(
while_stmt.condition().map(|n| n.kind()),
Some(SyntaxKind::CALL_EXPR)
);
assert_eq!(
while_stmt
.as_binding()
.and_then(|b| b.name_token())
.map(|t| t.text().to_string()),
Some("item".to_string())
);
}
#[test]
fn as_binding_followed_by_an_operator_is_a_named_parse_error() {
for src in [
"var x = { if find(s, \"a\") as i && i > 0 { 1; } 0 }\n",
"var x = { if find(s, \"a\") as i || true { 1; } 0 }\n",
"var x = { if find(s, \"a\") as i or 3 { 1; } 0 }\n",
] {
let p = parse(src);
assert_eq!(src, p.syntax().text().to_string(), "lossless round-trip");
assert!(
p.errors()
.iter()
.any(|e| e.message.contains("must be the entire condition")),
"expected the whole-condition error for {src:?}, got: {:?}",
p.errors()
);
}
}
#[test]
fn error_as_with_no_name_does_not_panic() {
let src = "var x = { if a as { 1; } 0 }\n";
let p = parse(src);
assert_eq!(src, p.syntax().text().to_string(), "lossless round-trip");
assert!(!p.errors().is_empty());
}
#[test]
fn logic_line_assignment_is_not_swallowed_as_prose() {
let src = "flow greet() {\n~ n = 5\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let flow: ast::FlowDecl = find_child(&p.syntax()).expect("flow decl");
let body = expect_prose_body(flow.body());
assert!(
!has_node_kind(body.syntax(), SyntaxKind::TEXT),
"the logic line must not be swallowed into a TEXT run"
);
let logic_line: ast::LogicLine = find_child(body.syntax()).expect("LOGIC_LINE");
let assign = logic_line.assign_stmt().expect("ASSIGN_STMT child");
assert!(logic_line.expr_stmt().is_none());
let place: ast::Path = find_child(assign.syntax()).expect("place path");
assert_eq!(
place
.segments()
.map(|t| t.text().to_string())
.collect::<Vec<_>>(),
vec!["n".to_string()]
);
assert_eq!(
assign.value().map(|n| n.kind()),
Some(SyntaxKind::INTEGER_LIT)
);
assert!(assign.op_token().is_none_or(|t| t.kind() == SyntaxKind::EQ));
}
#[test]
fn logic_line_temp_decl_is_not_swallowed_as_prose() {
let src = "flow greet() {\n~ let n = 5\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let flow: ast::FlowDecl = find_child(&p.syntax()).expect("flow decl");
let body = expect_prose_body(flow.body());
assert!(
!has_node_kind(body.syntax(), SyntaxKind::TEXT),
"the logic line must not be swallowed into a TEXT run"
);
let logic_line: ast::LogicLine = find_child(body.syntax()).expect("LOGIC_LINE");
let let_stmt = logic_line.let_stmt().expect("LET_STMT child");
assert!(logic_line.assign_stmt().is_none());
assert!(logic_line.expr_stmt().is_none());
assert_eq!(
let_stmt.name_token().map(|t| t.text().to_string()),
Some("n".to_string())
);
assert_eq!(
let_stmt.value().map(|n| n.kind()),
Some(SyntaxKind::INTEGER_LIT)
);
}
#[test]
fn logic_line_temp_decl_with_annotation_and_no_initializer() {
let src = "flow greet() {\n~ let n: int\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let flow: ast::FlowDecl = find_child(&p.syntax()).expect("flow decl");
let body = expect_prose_body(flow.body());
let logic_line: ast::LogicLine = find_child(body.syntax()).expect("LOGIC_LINE");
let let_stmt = logic_line.let_stmt().expect("LET_STMT child");
assert!(let_stmt.type_annotation().is_some());
assert!(let_stmt.value().is_none());
}
#[test]
fn logic_line_temp_decl_precedes_ordinary_content_on_the_next_line() {
let src = "flow greet() {\n~ let n = 5\nValue is {n}.\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let flow: ast::FlowDecl = find_child(&p.syntax()).expect("flow decl");
let body = expect_prose_body(flow.body());
let items: Vec<_> = body.syntax().children().collect();
assert_eq!(items[0].kind(), SyntaxKind::LOGIC_LINE);
assert_eq!(items[1].kind(), SyntaxKind::CONTENT_LINE);
}
#[test]
fn logic_line_compound_assignment_operators() {
for (src_op, expected) in [("+=", SyntaxKind::PLUS_EQ), ("-=", SyntaxKind::MINUS_EQ)] {
let src = format!("flow greet() {{\n~ n {src_op} 1\n}}\n");
let p = assert_lossless(&src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let flow: ast::FlowDecl = find_child(&p.syntax()).expect("flow decl");
let body = expect_prose_body(flow.body());
let logic_line: ast::LogicLine = find_child(body.syntax()).expect("LOGIC_LINE");
let assign = logic_line.assign_stmt().expect("ASSIGN_STMT child");
assert_eq!(assign.op_token().map(|t| t.kind()), Some(expected));
}
}
#[test]
fn logic_line_bare_expression_is_a_call() {
let src = "flow greet() {\n~ bump()\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let flow: ast::FlowDecl = find_child(&p.syntax()).expect("flow decl");
let body = expect_prose_body(flow.body());
let logic_line: ast::LogicLine = find_child(body.syntax()).expect("LOGIC_LINE");
assert!(logic_line.assign_stmt().is_none());
let expr_stmt = logic_line.expr_stmt().expect("EXPR_STMT child");
assert_eq!(
expr_stmt.expr().map(|n| n.kind()),
Some(SyntaxKind::CALL_EXPR)
);
}
#[test]
fn logic_line_precedes_ordinary_content_on_the_next_line() {
let src = "flow greet() {\n~ n = 5\nValue is {n}.\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let flow: ast::FlowDecl = find_child(&p.syntax()).expect("flow decl");
let body = expect_prose_body(flow.body());
let items: Vec<_> = body.syntax().children().collect();
assert_eq!(items[0].kind(), SyntaxKind::LOGIC_LINE);
assert_eq!(items[1].kind(), SyntaxKind::CONTENT_LINE);
assert!(has_node_kind(&items[1], SyntaxKind::INTERPOLATION));
}
#[test]
fn logic_line_inside_choice_body_and_conditional_colon_body() {
let choice_src = "flow greet() {\n{?\n* [go] {\n~ n = 1\n}\n}\n}\n";
let p = assert_lossless(choice_src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::LOGIC_LINE));
let colon_src = "flow greet() {\n{if n > 0: ~ n = 0}\n}\n";
let p = assert_lossless(colon_src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::LOGIC_LINE));
}
#[test]
fn logic_line_temp_decl_inside_choice_body_and_conditional_colon_body() {
let choice_src = "flow greet() {\n{?\n* [go] {\n~ let n = 1\n}\n}\n}\n";
let p = assert_lossless(choice_src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::LOGIC_LINE));
let colon_src = "flow greet() {\n{if n > 0: ~ let m = 0}\n}\n";
let p = assert_lossless(colon_src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::LOGIC_LINE));
}
#[test]
fn logic_line_temp_decl_partial_progress_is_not_swallowed_as_prose() {
let src = "flow greet() {\n~ let n 5\n}\n";
let p = parse(src);
assert_eq!(src, p.syntax().text().to_string(), "lossless round-trip");
assert!(
!p.errors().is_empty(),
"partial-progress leftover tokens must raise a real diagnostic"
);
assert!(
!has_node_kind(&p.syntax(), SyntaxKind::TEXT),
"partial-progress leftover tokens must never be swallowed into TEXT prose"
);
}
#[test]
fn logic_line_partial_progress_is_not_swallowed_as_prose() {
let src = "flow greet() {\n~ n *= 3\n}\n";
let p = parse(src);
assert_eq!(src, p.syntax().text().to_string(), "lossless round-trip");
assert!(
!p.errors().is_empty(),
"partial-progress leftover tokens must raise a real diagnostic"
);
assert!(
!has_node_kind(&p.syntax(), SyntaxKind::TEXT),
"partial-progress leftover tokens must never be swallowed into TEXT prose"
);
}
#[test]
fn logic_line_recovery_does_not_consume_enclosing_close_brace() {
let src = "flow greet() {\n{if n > 0: ~ if}\nafter\n}\n";
let p = parse(src);
assert_eq!(src, p.syntax().text().to_string(), "lossless round-trip");
assert_eq!(
p.errors().len(),
2,
"expected exactly the atom + logic-line diagnostics for `~ if`, no R_BRACE/EOF fallout: {:?}",
p.errors()
);
let flow: ast::FlowDecl = find_child(&p.syntax()).expect("flow decl");
let body = expect_prose_body(flow.body());
let items: Vec<_> = body.syntax().children().collect();
assert!(
items
.iter()
.any(|n| n.kind() == SyntaxKind::CONDITIONAL_BLOCK),
"expected a closed CONDITIONAL_BLOCK sibling, got: {:?}",
items.iter().map(SyntaxNode::kind).collect::<Vec<_>>()
);
assert!(
items.iter().any(|n| n.kind() == SyntaxKind::CONTENT_LINE
&& n.text().to_string().contains("after")),
"expected the following content line as a sibling, not absorbed into the block"
);
}
#[test]
fn logic_line_unsupported_shape_is_a_loud_diagnostic_never_silent_prose() {
let src = "flow greet() {\n~ if\n}\n";
let p = parse(src);
assert_eq!(src, p.syntax().text().to_string(), "lossless round-trip");
assert!(
!p.errors().is_empty(),
"an unsupported logic-line shape must raise a real diagnostic"
);
assert!(
!has_node_kind(&p.syntax(), SyntaxKind::TEXT),
"an unsupported logic-line shape must never be swallowed into TEXT prose"
);
}
#[test]
fn logic_line_until_is_not_swallowed_as_prose() {
let src = "flow greet() {\n~ until n > 0\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let flow: ast::FlowDecl = find_child(&p.syntax()).expect("flow decl");
let body = expect_prose_body(flow.body());
assert!(
!has_node_kind(body.syntax(), SyntaxKind::TEXT),
"the logic line must not be swallowed into a TEXT run"
);
let logic_line: ast::LogicLine = find_child(body.syntax()).expect("LOGIC_LINE");
let until_stmt = logic_line.until_stmt().expect("UNTIL_STMT child");
assert!(logic_line.let_stmt().is_none());
assert!(logic_line.assign_stmt().is_none());
assert!(logic_line.expr_stmt().is_none());
assert!(logic_line.stmt_block().is_none());
assert!(until_stmt.condition().is_some());
}
#[test]
fn logic_line_until_precedes_ordinary_content_on_the_next_line() {
let src = "flow greet() {\n~ until n > 0\nValue is {n}.\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let flow: ast::FlowDecl = find_child(&p.syntax()).expect("flow decl");
let body = expect_prose_body(flow.body());
let items: Vec<_> = body.syntax().children().collect();
assert_eq!(items[0].kind(), SyntaxKind::LOGIC_LINE);
assert_eq!(items[1].kind(), SyntaxKind::CONTENT_LINE);
}
#[test]
fn logic_line_until_inside_choice_body_and_conditional_colon_body() {
let choice_src = "flow greet() {\n{?\n* [go] {\n~ until n > 0\n}\n}\n}\n";
let p = assert_lossless(choice_src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::LOGIC_LINE));
let colon_src = "flow greet() {\n{if n > 0: ~ until n > 1}\n}\n";
let p = assert_lossless(colon_src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::LOGIC_LINE));
}
#[test]
fn logic_line_block_wraps_a_stmt_block_with_multiple_statements() {
let src = "flow greet() {\n~{\n let m = 1;\n n = m;\n}\nValue is {n}.\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let flow: ast::FlowDecl = find_child(&p.syntax()).expect("flow decl");
let body = expect_prose_body(flow.body());
let items: Vec<_> = body.syntax().children().collect();
assert_eq!(items[0].kind(), SyntaxKind::LOGIC_LINE);
assert_eq!(items[1].kind(), SyntaxKind::CONTENT_LINE);
let logic_line: ast::LogicLine = find_child(body.syntax()).expect("LOGIC_LINE");
let stmt_block = logic_line.stmt_block().expect("STMT_BLOCK child");
assert!(logic_line.until_stmt().is_none());
assert!(logic_line.let_stmt().is_none());
let inner_items: Vec<_> = stmt_block.items().collect();
assert_eq!(
inner_items.len(),
2,
"expected two statements inside ~{{ }}"
);
assert_eq!(inner_items[0].kind(), SyntaxKind::LET_STMT);
assert_eq!(inner_items[1].kind(), SyntaxKind::ASSIGN_STMT);
}
#[test]
fn logic_line_block_tolerates_a_space_between_tilde_and_brace() {
let src = "flow greet() {\n~ {\n n = 1;\n}\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::STMT_BLOCK));
}
#[test]
fn logic_line_block_inside_choice_body_and_conditional_colon_body() {
let choice_src = "flow greet() {\n{?\n* [go] {\n~{\n n = 1;\n}\n}\n}\n}\n";
let p = assert_lossless(choice_src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::STMT_BLOCK));
let colon_src = "flow greet() {\n{if n > 0: ~{ n = 0; }}\n}\n";
let p = assert_lossless(colon_src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::STMT_BLOCK));
}
#[test]
fn logic_line_block_unclosed_at_eof_does_not_panic() {
let src = "flow greet() {\n~{\n let m = 1;\n";
let p = parse(src);
assert_eq!(src, p.syntax().text().to_string(), "lossless round-trip");
assert!(!p.errors().is_empty());
}
#[test]
fn prose_line_wraps_a_content_line_with_zero_errors() {
let src = "fn radio() {\n> hi\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let f: ast::FnDecl = find_child(&p.syntax()).expect("fn decl");
let body = expect_code_body(f.body());
let items: Vec<_> = body.items().collect();
assert_eq!(items.len(), 1);
assert_eq!(items[0].kind(), SyntaxKind::PROSE_LINE);
let prose_line: ast::ProseLine = find_child(body.syntax()).expect("PROSE_LINE");
let content_line = prose_line.content_line().expect("CONTENT_LINE child");
assert!(has_node_kind(content_line.syntax(), SyntaxKind::TEXT));
}
#[test]
fn prose_line_carries_interpolation_like_any_content_line() {
let src = "fn radio(chan: string, text: string) {\n> [{chan}] {text}\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let f: ast::FnDecl = find_child(&p.syntax()).expect("fn decl");
let body = expect_code_body(f.body());
let prose_line: ast::ProseLine = find_child(body.syntax()).expect("PROSE_LINE");
let content_line = prose_line.content_line().expect("CONTENT_LINE child");
let interpolations: Vec<_> = content_line
.syntax()
.children()
.filter(|n| n.kind() == SyntaxKind::INTERPOLATION)
.collect();
assert_eq!(interpolations.len(), 2, "one per `{{…}}` interpolation");
}
#[test]
fn prose_line_precedes_ordinary_code_on_the_next_line() {
let src = "fn radio() {\n> hi\nn = 1;\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let f: ast::FnDecl = find_child(&p.syntax()).expect("fn decl");
let body = expect_code_body(f.body());
let items: Vec<_> = body.items().collect();
assert_eq!(items.len(), 2);
assert_eq!(items[0].kind(), SyntaxKind::PROSE_LINE);
assert_eq!(items[1].kind(), SyntaxKind::ASSIGN_STMT);
}
#[test]
fn prose_line_reachable_inside_nested_if_body() {
let src = "fn radio() {\nif true {\n> hi\n}\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::PROSE_LINE));
}
#[test]
fn prose_line_recovery_does_not_consume_enclosing_close_brace() {
let src = "fn radio() {\nif true {\n> hi\n}\nafter();\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let f: ast::FnDecl = find_child(&p.syntax()).expect("fn decl");
let body = expect_code_body(f.body());
let items: Vec<_> = body.items().collect();
assert_eq!(
items.len(),
2,
"expected IF_STMT + the trailing EXPR_STMT as siblings, got: {:?}",
items.iter().map(SyntaxNode::kind).collect::<Vec<_>>()
);
assert_eq!(items[0].kind(), SyntaxKind::IF_STMT);
assert_eq!(items[1].kind(), SyntaxKind::EXPR_STMT);
}
#[test]
fn prose_line_is_reachable_from_a_flows_compound_guard_body_too() {
let src = "flow greet() ~{\n> hi\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::PROSE_LINE));
}