use std::sync::Arc;
use brink_compiler::{AnalysisOptions, Dialect};
use brink_runtime::{DotNetRng, Step, Story};
#[expect(clippy::unwrap_used)]
fn compile(src: &str) -> brink_format::StoryData {
let options = AnalysisOptions {
dialect: Dialect::Brink,
..AnalysisOptions::default()
};
brink_compiler::compile_with_options("main.ink", |_p| Ok(src.to_owned()), options)
.unwrap()
.data
}
#[expect(clippy::unwrap_used)]
fn link_story(data: &brink_format::StoryData) -> Story<DotNetRng> {
let (program, line_tables) = brink_runtime::link(data).unwrap();
Story::new(Arc::new(program), line_tables)
}
#[expect(clippy::unwrap_used)]
fn run_to_terminal(story: &mut Story<DotNetRng>) -> String {
let mut out = String::new();
for step in story.continue_maximally().unwrap() {
if let Step::Line(line) = step {
out.push_str(&line.text);
}
}
out
}
#[test]
fn module_rename_rebinds_fn_token_divert_value_and_visit_count() {
let v1_src = r"
#@module(quest_3)
VAR target = -> ambush
VAR greeter = #fn(greet)
-> ambush
=== ambush ===
Ambush!
-> DONE
=== function greet(): int ===
~ return 42
=== reader ===
{READ_COUNT(-> ambush)}
-> DONE
";
let v1_data = compile(v1_src);
let mut v1 = link_story(&v1_data);
run_to_terminal(&mut v1);
let save = v1.save_state();
assert_eq!(
save.visits.len(),
1,
"ambush should have accrued exactly one visit: {:?}",
save.visits
);
let v2_src = r"
#@module(quest_4)
#@was(quest_3)
VAR target = -> ambush
VAR greeter = #fn(greet)
-> reader
=== ambush ===
Ambush!
-> DONE
=== function greet(): int ===
~ return 42
=== reader ===
visits={READ_COUNT(-> ambush)}
divert={target == -> ambush:same|different}
~ temp g = greeter()
fn={g}
-> DONE
";
let v2_data = compile(v2_src);
let mut v2 = link_story(&v2_data);
let report = v2.load_state(&save);
assert!(
report.is_clean(),
"module rename with #@was must rebind cleanly: {report:?}"
);
let output = run_to_terminal(&mut v2);
assert!(
output.contains("visits=1"),
"loaded visit count must rebind to the renamed module's ambush: {output:?}"
);
assert!(
output.contains("divert=same"),
"loaded divert-target value must rebind to the renamed module's ambush: {output:?}"
);
assert!(
output.contains("fn=42"),
"loaded fn token must rebind and dispatch to the renamed module's greet: {output:?}"
);
}
#[test]
fn declared_module_var_rename_rebinds_by_saved_id() {
let v1_src = r"
#@module(quest_5)
VAR anchor = 0
VAR score = 7
-> reader
=== reader ===
{score}
-> DONE
";
let v1_data = compile(v1_src);
let v1 = link_story(&v1_data);
let save = v1.save_state();
assert_eq!(
save.globals.get("score"),
Some(&brink_format::Value::Int(7)),
"v1 should save score=7: {:?}",
save.globals
);
let v2_src = r"
#@module(quest_5)
VAR anchor = 0
#@was(score)
VAR points = 99
-> reader
=== reader ===
{points}
-> DONE
";
let v2_data = compile(v2_src);
let mut v2 = link_story(&v2_data);
let report = v2.load_state(&save);
assert!(
report.is_clean(),
"declared-module VAR rename with #@was must rebind cleanly: {report:?}"
);
let output = run_to_terminal(&mut v2);
assert_eq!(
output.trim(),
"7",
"loaded value must rebind from the renamed declared-module global: {output:?}"
);
}
#[test]
fn module_rename_deep_rebinds_list_value() {
let v1_src = r"
#@module(quest_7)
LIST loot = sword, shield, potion
VAR held = (sword, potion)
-> reader
=== reader ===
{held}
-> DONE
";
let v1_data = compile(v1_src);
let v1 = link_story(&v1_data);
let save = v1.save_state();
let v2_src = r"
#@module(quest_8)
#@was(quest_7)
LIST loot = sword, shield, potion
VAR held = ()
-> reader
=== reader ===
{held}
-> DONE
";
let v2_data = compile(v2_src);
let mut v2 = link_story(&v2_data);
let report = v2.load_state(&save);
assert!(
report.is_clean(),
"module rename must deep-rebind a saved Value::List cleanly: {report:?}"
);
let output = run_to_terminal(&mut v2);
assert_eq!(
output.trim(),
"sword, potion",
"loaded LIST value's active items must rebind to the renamed module's list: {output:?}"
);
}
#[test]
fn knot_rename_rebinds_visit_count_and_divert_value() {
let v1_src = r"
VAR target = -> old_name
-> old_name
=== old_name ===
Here.
-> DONE
=== reader ===
{READ_COUNT(-> old_name)}
-> DONE
";
let v1_data = compile(v1_src);
let mut v1 = link_story(&v1_data);
run_to_terminal(&mut v1);
let save = v1.save_state();
assert_eq!(save.visits.len(), 1, "old_name should have one visit");
let v2_src = r"
VAR target = -> new_name
-> reader
=== new_name ===
#@was(old_name)
Here.
-> DONE
=== reader ===
visits={READ_COUNT(-> new_name)}
divert={target == -> new_name:same|different}
-> DONE
";
let v2_data = compile(v2_src);
let mut v2 = link_story(&v2_data);
let report = v2.load_state(&save);
assert!(
report.is_clean(),
"knot rename with #@was must rebind cleanly: {report:?}"
);
let output = run_to_terminal(&mut v2);
assert!(
output.contains("visits=1"),
"loaded visit count must rebind to the renamed knot: {output:?}"
);
assert!(
output.contains("divert=same"),
"loaded divert-target value must rebind to the renamed knot: {output:?}"
);
}
#[test]
fn knot_rename_rebinds_descendant_stitch_visit_count_and_divert_value() {
let v1_src = r"
VAR target = -> old_name.market
-> old_name.market
=== old_name ===
Here.
-> DONE
= market
Market.
-> DONE
=== reader ===
{READ_COUNT(-> old_name.market)}
-> DONE
";
let v1_data = compile(v1_src);
let mut v1 = link_story(&v1_data);
run_to_terminal(&mut v1);
let save = v1.save_state();
assert_eq!(
save.visits.len(),
1,
"old_name.market should have one visit"
);
let v2_src = r"
VAR target = -> new_name.market
-> reader
=== new_name ===
#@was(old_name)
Here.
-> DONE
= market
Market.
-> DONE
=== reader ===
visits={READ_COUNT(-> new_name.market)}
divert={target == -> new_name.market:same|different}
-> DONE
";
let v2_data = compile(v2_src);
let mut v2 = link_story(&v2_data);
let report = v2.load_state(&save);
assert!(
report.is_clean(),
"knot rename with #@was must transitively rebind a descendant stitch's visit count: {report:?}"
);
let output = run_to_terminal(&mut v2);
assert!(
output.contains("visits=1"),
"loaded descendant stitch visit count must rebind to the renamed knot's stitch: {output:?}"
);
assert!(
output.contains("divert=same"),
"loaded divert-target value must rebind to the renamed knot's descendant stitch: {output:?}"
);
}
#[test]
fn knot_rename_without_was_stays_orphaned() {
let v1_src = r"
VAR target = -> old_name
-> old_name
=== old_name ===
Here.
-> DONE
=== reader ===
{READ_COUNT(-> old_name)}
-> DONE
";
let v1_data = compile(v1_src);
let mut v1 = link_story(&v1_data);
run_to_terminal(&mut v1);
let save = v1.save_state();
let v2_src = r"
VAR target = -> new_name
-> reader
=== new_name ===
Here.
-> DONE
=== reader ===
visits={READ_COUNT(-> new_name)}
divert={target == -> new_name:same|different}
-> DONE
";
let v2_data = compile(v2_src);
let mut v2 = link_story(&v2_data);
let _report = v2.load_state(&save);
let output = run_to_terminal(&mut v2);
assert!(
output.contains("visits=0"),
"without #@was the visit count stays orphaned under the old id: {output:?}"
);
assert!(
output.contains("divert=different"),
"without #@was the divert-target value stays the stale id: {output:?}"
);
}