use super::*;
#[test]
fn choice_point_parses() {
let src = "flow garden() {\n {?\n * [Look] You look around.\n + (again) [Look again] Still a garden.\n else { Nothing left to do. }\n }\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
}
#[test]
fn space_after_choice_bracket_close_survives_inside_the_text_node() {
let src = "flow f() {\n {?\n * 'A wager!'[] I returned.\n }\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let inner = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::CHOICE_INNER_CONTENT)
.expect("CHOICE_INNER_CONTENT");
let inner_text = text_run_concat(&inner);
assert_eq!(
inner_text, " I returned.",
"space after `]` must be folded into the inner TEXT node"
);
let choice = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::CHOICE)
.expect("CHOICE");
let start = choice
.descendants()
.find(|n| n.kind() == SyntaxKind::CHOICE_START_CONTENT)
.expect("CHOICE_START_CONTENT");
let display = format!("{}{}", text_run_concat(&start), inner_text);
assert_eq!(display, "'A wager!' I returned.");
}
#[test]
fn charter_wager_shape_preserves_space_after_bracket_close() {
let src = concat!(
"flow f() {\n",
" {?\n",
" * 'A wager!'[] I returned. {\n",
" -> f\n",
" }\n",
" }\n",
"}\n",
);
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let inner = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::CHOICE_INNER_CONTENT)
.expect("CHOICE_INNER_CONTENT");
assert_eq!(text_run_concat(&inner), " I returned. ");
assert!(has_node_kind(&p.syntax(), SyntaxKind::CHOICE_BODY));
}
#[test]
fn whitespace_only_inner_content_makes_no_spurious_text_node() {
let src = "flow f() {\n {?\n * choose[] \n }\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let inner = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::CHOICE_INNER_CONTENT)
.expect("CHOICE_INNER_CONTENT");
assert!(
!has_node_kind(&inner, SyntaxKind::TEXT),
"whitespace-only inner content must not create a TEXT node, tree: {inner:#?}"
);
}
#[test]
fn choice_line_interpolation_before_bracket_parses_as_interpolation() {
let src = "flow f() {\n {?\n * Gold: {gold}\n }\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let choice = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::CHOICE)
.expect("CHOICE");
assert!(has_node_kind(&choice, SyntaxKind::INTERPOLATION));
assert!(!has_node_kind(&choice, SyntaxKind::CHOICE_BODY));
}
#[test]
fn choice_line_interpolation_inside_bracket_inner_content_parses() {
let src = "flow f() {\n {?\n * [Buy] You have {gold} left.\n }\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let inner = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::CHOICE_INNER_CONTENT)
.expect("CHOICE_INNER_CONTENT");
assert!(has_node_kind(&inner, SyntaxKind::INTERPOLATION));
}
#[test]
fn choice_body_still_opens_as_a_body_not_interpolation() {
let src = "flow f() {\n {?\n * [Eat] {\n You eat. -> f\n }\n }\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::CHOICE_BODY));
}
#[test]
fn choice_line_conditional_guard_and_trailing_interpolation_coexist() {
let src = "flow f() {\n {?\n * {if hp > 0} Gold: {gold}\n }\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let choice = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::CHOICE)
.expect("CHOICE");
assert!(has_node_kind(&choice, SyntaxKind::CHOICE_GUARD));
assert!(has_node_kind(&choice, SyntaxKind::INTERPOLATION));
}
#[test]
fn star_bullet_choice_standalone() {
let src = "flow f() {\n {?\n * Choice text\n }\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let bullet = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::CHOICE_BULLET)
.expect("CHOICE_BULLET");
assert_eq!(bullet.text().to_string(), "*");
}
#[test]
fn plus_bullet_choice_standalone() {
let src = "flow f() {\n {?\n + Choice text\n }\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let bullet = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::CHOICE_BULLET)
.expect("CHOICE_BULLET");
assert_eq!(bullet.text().to_string(), "+");
}
#[test]
fn star_and_plus_bullets_mixed_within_one_choice_point() {
let src = "flow f() {\n {?\n * a\n + b\n * c\n }\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::CHOICE), 3);
let bullets: Vec<String> = p
.syntax()
.descendants()
.filter(|n| n.kind() == SyntaxKind::CHOICE_BULLET)
.map(|n| n.text().to_string())
.collect();
assert_eq!(bullets, vec!["*", "+", "*"]);
}
#[test]
fn stacked_bullets_do_not_open_a_second_choice() {
let src = "flow f() {\n {?\n ++[text] inner\n }\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert_eq!(
count_node_kind(&p.syntax(), SyntaxKind::CHOICE),
1,
"the second `+` must not open a second CHOICE node"
);
let start = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::CHOICE_START_CONTENT)
.expect("CHOICE_START_CONTENT");
assert_eq!(text_run_concat(&start), "+");
}
#[test]
fn choice_label_alone_produces_a_label_node() {
let src = "flow f() {\n {?\n * (myLabel) Choice text\n }\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let label = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::LABEL)
.expect("LABEL");
assert_eq!(label.text().to_string(), "(myLabel)");
}
#[test]
fn choice_guard_alone_produces_a_guard_node() {
let src = "flow f() {\n {?\n * {if x > 5} Choice text\n }\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::CHOICE_GUARD));
}
#[test]
fn choice_guard_and_label_combine_in_the_documented_order() {
let src = "flow f() {\n {?\n * {if visited} (again) Been here.\n }\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let choice = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::CHOICE)
.expect("CHOICE");
let guard = choice
.children()
.find(|n| n.kind() == SyntaxKind::CHOICE_GUARD)
.expect("CHOICE_GUARD");
let label = choice
.children()
.find(|n| n.kind() == SyntaxKind::LABEL)
.expect("LABEL");
assert!(
guard.text_range().start() < label.text_range().start(),
"guard must precede label in child order"
);
}
#[test]
fn label_before_guard_is_currently_not_recognized_as_a_choice_guard() {
let src = "flow f() {\n {?\n * (again) {if visited} Been here.\n }\n}\n";
let p = assert_lossless(src);
assert!(
!p.errors().is_empty(),
"the reversed order is expected to produce an error: {:?}",
p.errors()
);
let choice = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::CHOICE)
.expect("CHOICE");
assert!(!has_node_kind(&choice, SyntaxKind::CHOICE_GUARD));
assert!(has_node_kind(&choice, SyntaxKind::CONDITIONAL_BLOCK));
}
#[test]
fn three_part_bracket_split_anatomy_in_isolation() {
let src = "flow f() {\n {?\n * Start[middle]end\n }\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let choice = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::CHOICE)
.expect("CHOICE");
let start = choice
.children()
.find(|n| n.kind() == SyntaxKind::CHOICE_START_CONTENT)
.expect("CHOICE_START_CONTENT");
let bracket = choice
.children()
.find(|n| n.kind() == SyntaxKind::CHOICE_BRACKET_CONTENT)
.expect("CHOICE_BRACKET_CONTENT");
let inner = choice
.children()
.find(|n| n.kind() == SyntaxKind::CHOICE_INNER_CONTENT)
.expect("CHOICE_INNER_CONTENT");
assert_eq!(text_run_concat(&start), "Start");
assert_eq!(text_run_concat(&bracket), "middle");
assert_eq!(text_run_concat(&inner), "end");
}
#[test]
fn bracket_only_choice_with_no_start_or_inner_text() {
let src = "flow f() {\n {?\n * [hidden] shown\n }\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let choice = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::CHOICE)
.expect("CHOICE");
let start = choice
.children()
.find(|n| n.kind() == SyntaxKind::CHOICE_START_CONTENT)
.expect("CHOICE_START_CONTENT");
assert_eq!(text_run_concat(&start), "");
let bracket = choice
.children()
.find(|n| n.kind() == SyntaxKind::CHOICE_BRACKET_CONTENT)
.expect("CHOICE_BRACKET_CONTENT");
assert_eq!(text_run_concat(&bracket), "hidden");
}
#[test]
fn choice_text_followed_by_divert_without_a_bracket() {
let src = "flow f() {\n {?\n * Choice -> knot\n }\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let choice = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::CHOICE)
.expect("CHOICE");
assert!(has_node_kind(&choice, SyntaxKind::DIVERT_STMT));
assert!(
!has_node_kind(&choice, SyntaxKind::CHOICE_BRACKET_CONTENT),
"no `[` was ever written, so no bracket anatomy should appear"
);
}
#[test]
fn bracket_split_anatomy_combined_with_a_divert_in_the_bracket_region() {
let src = "flow f() {\n {?\n * Go[-> elsewhere]stays\n }\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let bracket = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::CHOICE_BRACKET_CONTENT)
.expect("CHOICE_BRACKET_CONTENT");
assert!(has_node_kind(&bracket, SyntaxKind::DIVERT_STMT));
}
#[test]
fn choice_line_trailing_tag_is_recognized_as_a_tag_node() {
let src = "flow f() {\n {?\n * Choice #tag1\n }\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(
has_node_kind(&p.syntax(), SyntaxKind::TAG),
"the `#tag1` should be a real TAG node"
);
assert!(
!has_node_kind(&p.syntax(), SyntaxKind::ERROR),
"the `#tag1` must no longer be wrapped in ERROR nodes"
);
let choice = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::CHOICE)
.expect("CHOICE");
assert!(
has_node_kind(&choice, SyntaxKind::TAG),
"the TAG node should attach to the CHOICE the tag trails"
);
}
#[test]
fn choice_body_multiline_multi_region_content() {
let src = concat!(
"flow f() {\n",
" {?\n",
" * [Eat] {\n",
" You eat a donut.\n",
" Delicious.\n",
" }\n",
" }\n",
"}\n",
);
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let body = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::CHOICE_BODY)
.expect("CHOICE_BODY");
assert_eq!(count_node_kind(&body, SyntaxKind::CONTENT_LINE), 2);
}
#[test]
fn choice_body_may_nest_another_choice_point() {
let src = concat!(
"flow f() {\n",
" {?\n",
" * outer {\n",
" {?\n",
" * inner\n",
" }\n",
" }\n",
" }\n",
"}\n",
);
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::CHOICE_POINT), 2);
let outer_body = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::CHOICE_BODY)
.expect("CHOICE_BODY");
assert!(has_node_kind(&outer_body, SyntaxKind::CHOICE_POINT));
}
#[test]
fn else_branch_present_provides_a_fallback() {
let src = "flow f() {\n {?\n * a\n else { Nothing left. }\n }\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let point = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::CHOICE_POINT)
.expect("CHOICE_POINT");
let else_branch = point
.children()
.find(|n| n.kind() == SyntaxKind::ELSE_BRANCH)
.expect("ELSE_BRANCH");
assert!(has_node_kind(&else_branch, SyntaxKind::CHOICE_BODY));
}
#[test]
fn else_branch_absent_is_not_required() {
let src = "flow f() {\n {?\n * a\n * b\n }\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(!has_node_kind(&p.syntax(), SyntaxKind::ELSE_BRANCH));
}
#[test]
fn else_not_immediately_followed_by_a_brace_is_not_treated_as_an_else_branch_attempt() {
let src = "flow f() {\n {?\n * a\n else nope\n }\n}\n";
let p = assert_lossless(src);
assert!(!p.errors().is_empty());
assert!(
!has_node_kind(&p.syntax(), SyntaxKind::ELSE_BRANCH),
"no ELSE_BRANCH should be attempted when `else` isn't followed by `{{`"
);
assert!(has_node_kind(&p.syntax(), SyntaxKind::ERROR));
}
#[test]
fn splice_with_arguments_parses() {
let src = "flow f() {\n {?\n <- options(1, 2)\n }\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let splice = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::SPLICE)
.expect("SPLICE");
assert!(has_node_kind(&splice, SyntaxKind::ARG_LIST));
}
#[test]
fn splice_without_arguments_parses() {
let src = "flow f() {\n {?\n <- options\n }\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let splice = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::SPLICE)
.expect("SPLICE");
assert!(!has_node_kind(&splice, SyntaxKind::ARG_LIST));
}
#[test]
fn multiple_splices_mixed_with_choices_in_one_point() {
let src = "flow f() {\n {?\n * a\n <- shared_options()\n + b\n }\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::SPLICE), 1);
assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::CHOICE), 2);
}
#[test]
fn splice_missing_a_target_path_does_not_panic() {
let src = "flow f() {\n {?\n <-\n }\n}\n";
let p = assert_lossless(src);
assert!(!p.errors().is_empty());
assert!(has_node_kind(&p.syntax(), SyntaxKind::SPLICE));
}
#[test]
fn choice_point_with_no_choices_parses_with_no_errors() {
let src = "flow f() {\n {?\n }\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::CHOICE_POINT));
assert!(!has_node_kind(&p.syntax(), SyntaxKind::CHOICE));
}
#[test]
fn unclosed_choice_point_brace_recovers_without_panicking() {
let src = "flow f() {\n {?\n * a\n";
let p = assert_lossless(src);
assert!(!p.errors().is_empty());
assert!(has_node_kind(&p.syntax(), SyntaxKind::CHOICE_POINT));
}
#[test]
fn bullet_with_no_content_parses_with_an_empty_start_content() {
let src = "flow f() {\n {?\n *\n }\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let start = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::CHOICE_START_CONTENT)
.expect("CHOICE_START_CONTENT");
assert_eq!(text_run_concat(&start), "");
}
#[test]
fn unclosed_bracket_recovers_without_panicking() {
let src = "flow f() {\n {?\n * [text\n }\n}\n";
let p = assert_lossless(src);
assert!(!p.errors().is_empty());
assert!(has_node_kind(
&p.syntax(),
SyntaxKind::CHOICE_BRACKET_CONTENT
));
}
#[test]
fn malformed_guard_missing_condition_does_not_panic() {
let src = "flow f() {\n {?\n * {if} text\n }\n}\n";
let p = assert_lossless(src);
assert!(!p.errors().is_empty());
assert!(has_node_kind(&p.syntax(), SyntaxKind::CHOICE_GUARD));
}
#[test]
fn malformed_label_non_ident_falls_back_to_prose_without_panicking() {
let src = "flow f() {\n {?\n * (1) text\n }\n}\n";
let p = assert_lossless(src);
assert!(!p.errors().is_empty());
let label = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::LABEL)
.expect("LABEL");
assert!(!has_node_kind(&label, SyntaxKind::IDENT));
}
#[test]
fn garbage_inside_choice_point_recovers_token_by_token() {
let src = "flow f() {\n {?\n = = =\n * a\n }\n}\n";
let p = assert_lossless(src);
assert!(!p.errors().is_empty());
assert!(has_node_kind(&p.syntax(), SyntaxKind::ERROR));
assert!(has_node_kind(&p.syntax(), SyntaxKind::CHOICE));
}
#[test]
fn extremely_deep_nested_choice_points_do_not_overflow_the_stack() {
const LEVELS: usize = 150;
let mut src = String::new();
for _ in 0..LEVELS {
src.push_str("{?\n* a {\n");
}
src.push_str("* leaf\n");
for _ in 0..LEVELS {
src.push_str("}\n}\n");
}
let wrapped = format!("flow f() {{\n{src}}}\n");
let p = assert_lossless(&wrapped);
assert!(
p.errors()
.iter()
.any(|e| e.message.contains("maximum nesting depth exceeded")),
"expected the depth guard to trip: {:?}",
p.errors()
);
}
#[test]
fn adversarial_structural_soup_inside_a_choice_point_never_panics() {
let src = "flow f() {\n {?\n *(){}[]<-?{if}else\n }\n}\n";
assert_lossless(src);
}
mod choice_proptests {
use proptest::prelude::*;
use super::{SyntaxKind, has_node_kind, parse};
const KEYWORDS: &[&str] = &[
"pub", "flow", "fn", "var", "const", "let", "flags", "struct", "extern", "import", "use",
"module", "return", "ref", "if", "match", "else", "while", "for", "in", "until", "break",
"continue", "as", "or", "true", "false", "END", "DONE",
];
fn arb_ident() -> impl Strategy<Value = String> {
"[a-z][a-z0-9_]{0,6}"
.prop_filter("must not be a keyword", |s| !KEYWORDS.contains(&s.as_str()))
}
fn arb_bullet() -> impl Strategy<Value = &'static str> {
prop_oneof![Just("*"), Just("+")]
}
fn arb_full_choice_line() -> impl Strategy<Value = String> {
(
arb_bullet(),
proptest::option::of(arb_ident()),
proptest::option::of(arb_ident()),
arb_ident(),
proptest::option::of(arb_ident()),
)
.prop_map(|(bullet, guard_cond, label, start, bracket)| {
use std::fmt::Write as _;
let mut line = bullet.to_string();
if let Some(cond) = guard_cond {
let _ = write!(line, " {{if {cond}}}");
}
if let Some(name) = label {
let _ = write!(line, " ({name})");
}
line.push(' ');
line.push_str(&start);
if let Some(inner) = bracket {
let _ = write!(line, "[{inner}]");
}
line.push('\n');
line
})
}
fn arb_full_choice_point() -> impl Strategy<Value = String> {
prop::collection::vec(arb_full_choice_line(), 1..=4)
.prop_map(|lines| format!("{{?\n{}}}\n", lines.join("")))
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(128))]
#[test]
fn full_choice_point_roundtrips(point in arb_full_choice_point()) {
let src = format!("flow f() {{\n{point}}}\n");
let parsed = parse(&src);
prop_assert_eq!(parsed.syntax().text().to_string(), src);
}
#[test]
fn full_choice_point_never_panics_and_produces_a_choice_point_node(point in arb_full_choice_point()) {
let src = format!("flow f() {{\n{point}}}\n");
let parsed = parse(&src);
prop_assert!(has_node_kind(&parsed.syntax(), SyntaxKind::CHOICE_POINT));
}
#[test]
fn full_choice_point_produces_no_errors(point in arb_full_choice_point()) {
let src = format!("flow f() {{\n{point}}}\n");
let parsed = parse(&src);
prop_assert!(parsed.errors().is_empty(), "errors: {:?}", parsed.errors());
}
}
}