use std::collections::{BTreeMap, HashMap};
use std::sync::Arc;
use brink_format::{DefinitionId, Value};
use crate::program::Program;
use crate::rng::StoryRng;
use crate::state::ContextAccess;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Scope {
#[default]
World,
Local,
}
#[derive(Debug, Clone, Default)]
pub struct WorldPolicy {
pub default: Scope,
pub overrides: BTreeMap<String, Scope>,
pub turn_index: Scope,
pub rng: Scope,
}
#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
pub enum PolicyError {
#[error("unknown variable or knot/stitch in world policy overrides: {0}")]
UnknownName(String),
}
#[derive(Debug, Clone)]
pub struct ResolvedPolicy {
default: Scope,
global_scopes: Vec<Scope>,
knot_scopes: HashMap<DefinitionId, Scope>,
turn_index: Scope,
rng: Scope,
}
impl ResolvedPolicy {
#[must_use]
pub fn all_world() -> Self {
Self {
default: Scope::World,
global_scopes: Vec::new(),
knot_scopes: HashMap::new(),
turn_index: Scope::World,
rng: Scope::World,
}
}
pub fn resolve(program: &Program, policy: &WorldPolicy) -> Result<Self, PolicyError> {
if policy.overrides.is_empty()
&& policy.default == Scope::World
&& policy.turn_index == Scope::World
&& policy.rng == Scope::World
&& !program.has_local_defaults()
{
return Ok(Self::all_world());
}
let mut global_scopes: Vec<Scope> = (0..program.global_count())
.map(|slot| {
if program.global_is_local(slot) {
Scope::Local
} else {
policy.default
}
})
.collect();
let mut knot_scopes = HashMap::new();
let interior_by_scope = interior_containers_by_scope(program);
for (path, id) in program.local_scope_defaults() {
expand_knot_scope(
program,
&interior_by_scope,
&mut knot_scopes,
path,
*id,
Scope::Local,
);
}
for (name, &scope) in &policy.overrides {
if let Some(slot) = program.global_index(name) {
global_scopes[slot as usize] = scope;
} else if let Some(id) = program.find_path_target(name) {
expand_knot_scope(
program,
&interior_by_scope,
&mut knot_scopes,
name,
id,
scope,
);
} else {
return Err(PolicyError::UnknownName(name.clone()));
}
}
Ok(Self {
default: policy.default,
global_scopes,
knot_scopes,
turn_index: policy.turn_index,
rng: policy.rng,
})
}
#[must_use]
pub fn scope_of_global(&self, slot: u32) -> Scope {
self.global_scopes
.get(slot as usize)
.copied()
.unwrap_or(self.default)
}
#[must_use]
pub fn scope_of_knot(&self, id: DefinitionId) -> Scope {
self.knot_scopes.get(&id).copied().unwrap_or(self.default)
}
#[must_use]
pub fn turn_index_scope(&self) -> Scope {
self.turn_index
}
#[must_use]
pub fn rng_scope(&self) -> Scope {
self.rng
}
}
fn interior_containers_by_scope(program: &Program) -> HashMap<DefinitionId, Vec<DefinitionId>> {
let mut by_scope: HashMap<DefinitionId, Vec<DefinitionId>> = HashMap::new();
for (idx, container) in program.containers.iter().enumerate() {
#[expect(
clippy::cast_possible_truncation,
reason = "container count fits in u32"
)]
let idx = idx as u32;
let owner = program.scope_ids[program.scope_table_idx(idx) as usize];
if owner != container.id {
by_scope.entry(owner).or_default().push(container.id);
}
}
by_scope
}
fn apply_scope_to_subtree(
interior_by_scope: &HashMap<DefinitionId, Vec<DefinitionId>>,
knot_scopes: &mut HashMap<DefinitionId, Scope>,
id: DefinitionId,
scope: Scope,
) {
knot_scopes.insert(id, scope);
if let Some(interior) = interior_by_scope.get(&id) {
for &child_id in interior {
knot_scopes.insert(child_id, scope);
}
}
}
fn expand_knot_scope(
program: &Program,
interior_by_scope: &HashMap<DefinitionId, Vec<DefinitionId>>,
knot_scopes: &mut HashMap<DefinitionId, Scope>,
name: &str,
id: DefinitionId,
scope: Scope,
) {
apply_scope_to_subtree(interior_by_scope, knot_scopes, id, scope);
let prefix = format!("{name}.");
for (path, target) in &program.address_by_path {
let Some(rest) = path.strip_prefix(prefix.as_str()) else {
continue;
};
if rest.is_empty() || rest.contains('.') {
continue; }
if target.byte_offset != 0 {
continue; }
let owner = program.scope_ids[program.scope_table_idx(target.container_idx) as usize];
if owner != target.id {
continue;
}
apply_scope_to_subtree(interior_by_scope, knot_scopes, target.id, scope);
}
}
#[derive(Debug, Clone)]
pub struct World {
pub globals: Vec<Value>,
pub visit_counts: HashMap<DefinitionId, u32>,
pub turn_counts: HashMap<DefinitionId, u32>,
pub turn_index: u32,
pub rng_seed: i32,
pub previous_random: i32,
policy: Box<ResolvedPolicy>,
}
impl World {
pub fn new(program: &Program, policy: &WorldPolicy) -> Result<Self, PolicyError> {
let resolved = ResolvedPolicy::resolve(program, policy)?;
Ok(Self::from_globals(program.global_defaults(), resolved))
}
pub(crate) fn from_globals(globals: Vec<Value>, policy: ResolvedPolicy) -> Self {
Self {
globals,
visit_counts: HashMap::new(),
turn_counts: HashMap::new(),
turn_index: 0,
rng_seed: 0,
previous_random: 0,
policy: Box::new(policy),
}
}
#[cfg(feature = "testing")]
#[must_use]
pub fn new_for_testing(
globals: Vec<Value>,
visit_counts: HashMap<DefinitionId, u32>,
turn_counts: HashMap<DefinitionId, u32>,
turn_index: u32,
rng_seed: i32,
previous_random: i32,
) -> Self {
Self {
globals,
visit_counts,
turn_counts,
turn_index,
rng_seed,
previous_random,
policy: Box::new(ResolvedPolicy::all_world()),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct LocalRng {
pub seed: i32,
pub previous_random: i32,
}
#[derive(Debug, Clone, Default)]
pub struct FrozenLocal {
globals: BTreeMap<u32, Value>,
visit_counts: BTreeMap<DefinitionId, u32>,
turn_counts: BTreeMap<DefinitionId, u32>,
turn_index: Option<u32>,
rng: Option<LocalRng>,
base: Option<Arc<FrozenLocal>>,
}
impl FrozenLocal {
fn chain_get_global(&self, idx: u32) -> Option<&Value> {
self.globals
.get(&idx)
.or_else(|| self.base.as_deref().and_then(|b| b.chain_get_global(idx)))
}
fn chain_get_visit_count(&self, id: DefinitionId) -> Option<u32> {
self.visit_counts.get(&id).copied().or_else(|| {
self.base
.as_deref()
.and_then(|b| b.chain_get_visit_count(id))
})
}
fn chain_get_turn_count(&self, id: DefinitionId) -> Option<u32> {
self.turn_counts.get(&id).copied().or_else(|| {
self.base
.as_deref()
.and_then(|b| b.chain_get_turn_count(id))
})
}
fn chain_get_turn_index(&self) -> Option<u32> {
self.turn_index.or_else(|| {
self.base
.as_deref()
.and_then(FrozenLocal::chain_get_turn_index)
})
}
fn chain_get_rng(&self) -> Option<LocalRng> {
self.rng
.or_else(|| self.base.as_deref().and_then(FrozenLocal::chain_get_rng))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Mode {
#[default]
Normal,
Sandbox,
}
#[derive(Debug, Clone, Default)]
pub struct FlowLocal {
globals: BTreeMap<u32, Value>,
visit_counts: BTreeMap<DefinitionId, u32>,
turn_counts: BTreeMap<DefinitionId, u32>,
turn_index: Option<u32>,
rng: Option<LocalRng>,
base: Option<Arc<FrozenLocal>>,
mode: Mode,
}
impl FlowLocal {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
fn freeze(&self) -> Arc<FrozenLocal> {
Arc::new(FrozenLocal {
globals: self.globals.clone(),
visit_counts: self.visit_counts.clone(),
turn_counts: self.turn_counts.clone(),
turn_index: self.turn_index,
rng: self.rng,
base: self.base.clone(),
})
}
#[must_use]
pub fn fork(&self, mode: Mode) -> FlowLocal {
FlowLocal {
base: Some(self.freeze()),
mode,
..FlowLocal::new()
}
}
fn chain_get_global(&self, idx: u32) -> Option<&Value> {
self.globals
.get(&idx)
.or_else(|| self.base.as_deref().and_then(|b| b.chain_get_global(idx)))
}
fn chain_get_visit_count(&self, id: DefinitionId) -> Option<u32> {
self.visit_counts.get(&id).copied().or_else(|| {
self.base
.as_deref()
.and_then(|b| b.chain_get_visit_count(id))
})
}
fn chain_get_turn_count(&self, id: DefinitionId) -> Option<u32> {
self.turn_counts.get(&id).copied().or_else(|| {
self.base
.as_deref()
.and_then(|b| b.chain_get_turn_count(id))
})
}
fn chain_get_turn_index(&self) -> Option<u32> {
self.turn_index.or_else(|| {
self.base
.as_deref()
.and_then(FrozenLocal::chain_get_turn_index)
})
}
fn chain_get_rng(&self) -> Option<LocalRng> {
self.rng
.or_else(|| self.base.as_deref().and_then(FrozenLocal::chain_get_rng))
}
}
#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
pub enum CommitError {
#[error(
"commit is not implemented in this release; fork's only supported terminal operation is discard (drop the child)"
)]
NotImplemented,
}
pub fn commit(_child: FlowLocal, _parent: &mut FlowLocal) -> Result<(), CommitError> {
Err(CommitError::NotImplemented)
}
pub struct ContextView<'a> {
world: &'a mut World,
local: &'a mut FlowLocal,
}
impl<'a> ContextView<'a> {
pub fn new(world: &'a mut World, local: &'a mut FlowLocal) -> Self {
Self { world, local }
}
#[inline]
fn effective_scope(&self, policy_scope: Scope) -> Scope {
match self.local.mode {
Mode::Normal => policy_scope,
Mode::Sandbox => Scope::Local,
}
}
}
impl ContextAccess for World {
#[inline]
fn global(&self, idx: u32) -> &Value {
&self.globals[idx as usize]
}
#[inline]
fn set_global(&mut self, idx: u32, value: Value) {
self.globals[idx as usize] = value;
}
#[inline]
fn visit_count(&self, id: DefinitionId) -> u32 {
self.visit_counts.get(&id).copied().unwrap_or(0)
}
#[inline]
fn increment_visit(&mut self, id: DefinitionId) {
*self.visit_counts.entry(id).or_insert(0) += 1;
}
#[inline]
fn set_visit_count(&mut self, id: DefinitionId, count: u32) {
self.visit_counts.insert(id, count);
}
#[inline]
fn turn_count(&self, id: DefinitionId) -> Option<u32> {
self.turn_counts.get(&id).copied()
}
#[inline]
fn set_turn_count(&mut self, id: DefinitionId, turn: u32) {
self.turn_counts.insert(id, turn);
}
#[inline]
fn turn_index(&self) -> u32 {
self.turn_index
}
#[inline]
fn increment_turn_index(&mut self) {
self.turn_index += 1;
}
#[inline]
fn set_turn_index(&mut self, index: u32) {
self.turn_index = index;
}
#[inline]
fn rng_seed(&self) -> i32 {
self.rng_seed
}
#[inline]
fn set_rng_seed(&mut self, seed: i32) {
self.rng_seed = seed;
}
#[inline]
fn previous_random(&self) -> i32 {
self.previous_random
}
#[inline]
fn set_previous_random(&mut self, val: i32) {
self.previous_random = val;
}
#[inline]
fn next_random<R: StoryRng>(&self, seed: i32) -> i32 {
let mut rng = R::from_seed(seed);
rng.next_int()
}
fn random_sequence<R: StoryRng>(&self, seed: i32, count: usize) -> Vec<i32> {
let mut rng = R::from_seed(seed);
(0..count).map(|_| rng.next_int()).collect()
}
}
impl ContextAccess for ContextView<'_> {
#[inline]
fn global(&self, idx: u32) -> &Value {
match self.effective_scope(self.world.policy.scope_of_global(idx)) {
Scope::Local => self
.local
.chain_get_global(idx)
.unwrap_or_else(|| self.world.global(idx)),
Scope::World => self.world.global(idx),
}
}
#[inline]
fn set_global(&mut self, idx: u32, value: Value) {
match self.effective_scope(self.world.policy.scope_of_global(idx)) {
Scope::Local => {
self.local.globals.insert(idx, value);
}
Scope::World => self.world.set_global(idx, value),
}
}
#[inline]
fn visit_count(&self, id: DefinitionId) -> u32 {
match self.effective_scope(self.world.policy.scope_of_knot(id)) {
Scope::Local => self
.local
.chain_get_visit_count(id)
.unwrap_or_else(|| self.world.visit_count(id)),
Scope::World => self.world.visit_count(id),
}
}
#[inline]
fn increment_visit(&mut self, id: DefinitionId) {
match self.effective_scope(self.world.policy.scope_of_knot(id)) {
Scope::Local => {
let base = self.visit_count(id);
self.local.visit_counts.insert(id, base + 1);
}
Scope::World => self.world.increment_visit(id),
}
}
#[inline]
fn set_visit_count(&mut self, id: DefinitionId, count: u32) {
match self.effective_scope(self.world.policy.scope_of_knot(id)) {
Scope::Local => {
self.local.visit_counts.insert(id, count);
}
Scope::World => self.world.set_visit_count(id, count),
}
}
#[inline]
fn turn_count(&self, id: DefinitionId) -> Option<u32> {
match self.effective_scope(self.world.policy.scope_of_knot(id)) {
Scope::Local => self
.local
.chain_get_turn_count(id)
.or_else(|| self.world.turn_count(id)),
Scope::World => self.world.turn_count(id),
}
}
#[inline]
fn set_turn_count(&mut self, id: DefinitionId, turn: u32) {
match self.effective_scope(self.world.policy.scope_of_knot(id)) {
Scope::Local => {
self.local.turn_counts.insert(id, turn);
}
Scope::World => self.world.set_turn_count(id, turn),
}
}
#[inline]
fn turn_index(&self) -> u32 {
match self.effective_scope(self.world.policy.turn_index_scope()) {
Scope::Local => self
.local
.chain_get_turn_index()
.unwrap_or_else(|| self.world.turn_index()),
Scope::World => self.world.turn_index(),
}
}
#[inline]
fn increment_turn_index(&mut self) {
match self.effective_scope(self.world.policy.turn_index_scope()) {
Scope::Local => {
let base = self.turn_index();
self.local.turn_index = Some(base + 1);
}
Scope::World => self.world.increment_turn_index(),
}
}
#[inline]
fn set_turn_index(&mut self, index: u32) {
match self.effective_scope(self.world.policy.turn_index_scope()) {
Scope::Local => {
self.local.turn_index = Some(index);
}
Scope::World => self.world.set_turn_index(index),
}
}
#[inline]
fn rng_seed(&self) -> i32 {
match self.effective_scope(self.world.policy.rng_scope()) {
Scope::Local => self
.local
.chain_get_rng()
.map_or_else(|| self.world.rng_seed(), |rng| rng.seed),
Scope::World => self.world.rng_seed(),
}
}
#[inline]
fn set_rng_seed(&mut self, seed: i32) {
match self.effective_scope(self.world.policy.rng_scope()) {
Scope::Local => {
let fallback = self.local.chain_get_rng().unwrap_or(LocalRng {
seed: self.world.rng_seed(),
previous_random: self.world.previous_random(),
});
let rng = self.local.rng.get_or_insert(fallback);
rng.seed = seed;
}
Scope::World => self.world.set_rng_seed(seed),
}
}
#[inline]
fn previous_random(&self) -> i32 {
match self.effective_scope(self.world.policy.rng_scope()) {
Scope::Local => self
.local
.chain_get_rng()
.map_or_else(|| self.world.previous_random(), |rng| rng.previous_random),
Scope::World => self.world.previous_random(),
}
}
#[inline]
fn set_previous_random(&mut self, val: i32) {
match self.effective_scope(self.world.policy.rng_scope()) {
Scope::Local => {
let fallback = self.local.chain_get_rng().unwrap_or(LocalRng {
seed: self.world.rng_seed(),
previous_random: self.world.previous_random(),
});
let rng = self.local.rng.get_or_insert(fallback);
rng.previous_random = val;
}
Scope::World => self.world.set_previous_random(val),
}
}
#[inline]
fn next_random<R: StoryRng>(&self, seed: i32) -> i32 {
self.world.next_random::<R>(seed)
}
fn random_sequence<R: StoryRng>(&self, seed: i32, count: usize) -> Vec<i32> {
self.world.random_sequence::<R>(seed, count)
}
}
#[cfg(test)]
mod policy_tests {
use super::*;
use crate::link;
fn compile(src: &str) -> Program {
let out = brink_compiler::compile("t.ink", |p| {
if p == "t.ink" {
Ok(src.to_string())
} else {
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"no such include",
))
}
})
.expect("compile");
let (program, _line_tables) = link(&out.data).expect("link");
program
}
fn sample_program() -> Program {
compile(
"VAR gold = 0\n\
VAR mood = 0\n\
-> shrine\n\
=== shrine ===\n\
At the shrine.\n\
-> END\n\
=== cellar ===\n\
In the cellar.\n\
-> END\n",
)
}
#[test]
fn all_world_default_resolves_via_fast_path() {
let program = sample_program();
let policy = WorldPolicy::default();
let resolved = ResolvedPolicy::resolve(&program, &policy).expect("resolves");
assert!(resolved.global_scopes.is_empty());
assert!(resolved.knot_scopes.is_empty());
let gold_slot = program.global_index("gold").expect("gold declared");
let mood_slot = program.global_index("mood").expect("mood declared");
let shrine_id = program.find_path_target("shrine").expect("shrine exists");
let cellar_id = program.find_path_target("cellar").expect("cellar exists");
assert_eq!(resolved.scope_of_global(gold_slot), Scope::World);
assert_eq!(resolved.scope_of_global(mood_slot), Scope::World);
assert_eq!(resolved.scope_of_knot(shrine_id), Scope::World);
assert_eq!(resolved.scope_of_knot(cellar_id), Scope::World);
assert_eq!(resolved.turn_index_scope(), Scope::World);
assert_eq!(resolved.rng_scope(), Scope::World);
}
#[test]
fn resolves_valid_variable_and_knot_overrides() {
let program = sample_program();
let mut overrides = BTreeMap::new();
overrides.insert("gold".to_owned(), Scope::World);
overrides.insert("shrine".to_owned(), Scope::World);
let policy = WorldPolicy {
default: Scope::Local,
overrides,
turn_index: Scope::Local,
rng: Scope::Local,
};
let resolved = ResolvedPolicy::resolve(&program, &policy).expect("resolves");
let gold_slot = program.global_index("gold").expect("gold declared");
let mood_slot = program.global_index("mood").expect("mood declared");
let shrine_id = program.find_path_target("shrine").expect("shrine exists");
let cellar_id = program.find_path_target("cellar").expect("cellar exists");
assert_eq!(resolved.scope_of_global(gold_slot), Scope::World);
assert_eq!(resolved.scope_of_knot(shrine_id), Scope::World);
assert_eq!(resolved.scope_of_global(mood_slot), Scope::Local);
assert_eq!(resolved.scope_of_knot(cellar_id), Scope::Local);
assert_eq!(resolved.turn_index_scope(), Scope::Local);
assert_eq!(resolved.rng_scope(), Scope::Local);
}
#[test]
fn unknown_override_name_is_an_error() {
let program = sample_program();
let mut overrides = BTreeMap::new();
overrides.insert("not_a_real_name".to_owned(), Scope::World);
let policy = WorldPolicy {
default: Scope::World,
overrides,
turn_index: Scope::World,
rng: Scope::World,
};
let err = ResolvedPolicy::resolve(&program, &policy).expect_err("must fail");
assert_eq!(err, PolicyError::UnknownName("not_a_real_name".to_owned()));
}
#[test]
fn world_new_resolves_policy_and_initializes_globals() {
let program = sample_program();
let world = World::new(&program, &WorldPolicy::default()).expect("world builds");
assert_eq!(world.globals, program.global_defaults());
}
#[test]
fn world_new_propagates_unknown_name_error() {
let program = sample_program();
let mut overrides = BTreeMap::new();
overrides.insert("nonexistent".to_owned(), Scope::Local);
let policy = WorldPolicy {
overrides,
..WorldPolicy::default()
};
let err = World::new(&program, &policy).expect_err("must fail");
assert_eq!(err, PolicyError::UnknownName("nonexistent".to_owned()));
}
}
#[cfg(test)]
mod routing_tests {
use super::*;
use crate::link;
fn compile(src: &str) -> Program {
let out = brink_compiler::compile("t.ink", |p| {
if p == "t.ink" {
Ok(src.to_string())
} else {
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"no such include",
))
}
})
.expect("compile");
let (program, _line_tables) = link(&out.data).expect("link");
program
}
fn sample_program() -> Program {
compile(
"VAR gold = 0\n\
VAR mood = 0\n\
-> shrine\n\
=== shrine ===\n\
At the shrine.\n\
-> END\n\
=== cellar ===\n\
In the cellar.\n\
-> END\n",
)
}
fn mixed_policy() -> WorldPolicy {
let mut overrides = BTreeMap::new();
overrides.insert("mood".to_owned(), Scope::Local);
WorldPolicy {
default: Scope::World,
overrides,
turn_index: Scope::World,
rng: Scope::World,
}
}
#[test]
fn local_write_isolated_world_write_shared() {
let program = sample_program();
let policy = mixed_policy();
let mut world = World::new(&program, &policy).expect("world builds");
let gold_slot = program.global_index("gold").expect("gold declared");
let mood_slot = program.global_index("mood").expect("mood declared");
let mut local_a = FlowLocal::new();
let mut local_b = FlowLocal::new();
{
let mut view_a = ContextView::new(&mut world, &mut local_a);
view_a.set_global(mood_slot, Value::Int(42));
assert_eq!(view_a.global(mood_slot), &Value::Int(42));
}
{
let view_b = ContextView::new(&mut world, &mut local_b);
assert_eq!(view_b.global(mood_slot), &Value::Int(0));
assert_eq!(world.global(mood_slot), &Value::Int(0));
}
{
let mut view_a = ContextView::new(&mut world, &mut local_a);
view_a.set_global(gold_slot, Value::Int(7));
}
{
let view_b = ContextView::new(&mut world, &mut local_b);
assert_eq!(view_b.global(gold_slot), &Value::Int(7));
}
assert_eq!(world.global(gold_slot), &Value::Int(7));
}
#[test]
fn local_visits_independent_world_visits_shared() {
let program = sample_program();
let mut overrides = BTreeMap::new();
overrides.insert("shrine".to_owned(), Scope::Local);
let policy = WorldPolicy {
default: Scope::World,
overrides,
turn_index: Scope::World,
rng: Scope::World,
};
let mut world = World::new(&program, &policy).expect("world builds");
let shrine_id = program.find_path_target("shrine").expect("shrine exists");
let cellar_id = program.find_path_target("cellar").expect("cellar exists");
let mut local_a = FlowLocal::new();
let mut local_b = FlowLocal::new();
{
let mut view_a = ContextView::new(&mut world, &mut local_a);
view_a.increment_visit(shrine_id);
view_a.increment_visit(shrine_id);
assert_eq!(view_a.visit_count(shrine_id), 2);
}
{
let view_b = ContextView::new(&mut world, &mut local_b);
assert_eq!(view_b.visit_count(shrine_id), 0);
}
assert_eq!(world.visit_count(shrine_id), 0);
{
let mut view_a = ContextView::new(&mut world, &mut local_a);
view_a.increment_visit(cellar_id);
}
{
let view_b = ContextView::new(&mut world, &mut local_b);
assert_eq!(view_b.visit_count(cellar_id), 1);
}
assert_eq!(world.visit_count(cellar_id), 1);
}
#[test]
fn local_read_through_returns_world_default_before_first_write() {
let program = sample_program();
let policy = mixed_policy();
let mut world = World::new(&program, &policy).expect("world builds");
let mood_slot = program.global_index("mood").expect("mood declared");
world.set_global(mood_slot, Value::Int(99));
let mut local_a = FlowLocal::new();
let view_a = ContextView::new(&mut world, &mut local_a);
assert_eq!(view_a.global(mood_slot), &Value::Int(99));
}
#[test]
fn local_increment_is_cow_from_read_through_base() {
let program = sample_program();
let mut overrides = BTreeMap::new();
overrides.insert("shrine".to_owned(), Scope::Local);
let policy = WorldPolicy {
default: Scope::World,
overrides,
turn_index: Scope::World,
rng: Scope::World,
};
let mut world = World::new(&program, &policy).expect("world builds");
let shrine_id = program.find_path_target("shrine").expect("shrine exists");
world.increment_visit(shrine_id);
world.increment_visit(shrine_id);
assert_eq!(world.visit_count(shrine_id), 2);
let mut local_a = FlowLocal::new();
let mut view_a = ContextView::new(&mut world, &mut local_a);
view_a.increment_visit(shrine_id);
assert_eq!(view_a.visit_count(shrine_id), 3);
assert_eq!(world.visit_count(shrine_id), 2);
}
#[test]
fn chain_read_through_reads_base_and_top_shadows() {
let program = sample_program();
let mut overrides = BTreeMap::new();
overrides.insert("gold".to_owned(), Scope::Local);
overrides.insert("mood".to_owned(), Scope::Local);
overrides.insert("shrine".to_owned(), Scope::Local);
overrides.insert("cellar".to_owned(), Scope::Local);
let policy = WorldPolicy {
default: Scope::World,
overrides,
turn_index: Scope::Local,
rng: Scope::Local,
};
let mut world = World::new(&program, &policy).expect("world builds");
let gold_slot = program.global_index("gold").expect("gold declared");
let mood_slot = program.global_index("mood").expect("mood declared");
let shrine_id = program.find_path_target("shrine").expect("shrine exists");
let cellar_id = program.find_path_target("cellar").expect("cellar exists");
world.set_global(gold_slot, Value::Int(1));
world.set_global(mood_slot, Value::Int(1));
let mut parent = FlowLocal::new();
{
let mut pv = ContextView::new(&mut world, &mut parent);
pv.set_global(gold_slot, Value::Int(100));
pv.set_global(mood_slot, Value::Int(200));
pv.increment_visit(shrine_id); pv.set_turn_count(cellar_id, 5);
pv.increment_turn_index(); pv.set_rng_seed(777);
}
let base = parent.freeze();
let mut child = FlowLocal {
base: Some(base),
..FlowLocal::new()
};
{
let view = ContextView::new(&mut world, &mut child);
assert_eq!(view.global(gold_slot), &Value::Int(100));
assert_eq!(view.global(mood_slot), &Value::Int(200));
assert_eq!(view.visit_count(shrine_id), 1);
assert_eq!(view.turn_count(cellar_id), Some(5));
assert_eq!(view.turn_index(), 1);
assert_eq!(view.rng_seed(), 777);
}
{
let mut view = ContextView::new(&mut world, &mut child);
view.set_global(gold_slot, Value::Int(999));
assert_eq!(view.global(gold_slot), &Value::Int(999)); assert_eq!(view.global(mood_slot), &Value::Int(200)); }
{
let view = ContextView::new(&mut world, &mut child);
assert_eq!(view.visit_count(cellar_id), 0);
}
}
#[test]
fn fork_isolation_normal_child_write_does_not_leak_to_parent_or_world() {
let program = sample_program();
let policy = mixed_policy(); let mut world = World::new(&program, &policy).expect("world builds");
let mood_slot = program.global_index("mood").expect("mood declared");
let mut parent = FlowLocal::new();
{
let mut view = ContextView::new(&mut world, &mut parent);
view.set_global(mood_slot, Value::Int(42));
}
let mut child = parent.fork(Mode::Normal);
{
let view = ContextView::new(&mut world, &mut child);
assert_eq!(view.global(mood_slot), &Value::Int(42));
}
{
let mut view = ContextView::new(&mut world, &mut child);
view.set_global(mood_slot, Value::Int(100));
assert_eq!(view.global(mood_slot), &Value::Int(100));
}
{
let view = ContextView::new(&mut world, &mut parent);
assert_eq!(view.global(mood_slot), &Value::Int(42));
}
assert_eq!(world.global(mood_slot), &Value::Int(0));
}
#[test]
fn fork_base_is_a_frozen_snapshot_later_parent_writes_invisible_to_child() {
let program = sample_program();
let policy = mixed_policy(); let mut world = World::new(&program, &policy).expect("world builds");
let mood_slot = program.global_index("mood").expect("mood declared");
let mut parent = FlowLocal::new();
{
let mut view = ContextView::new(&mut world, &mut parent);
view.set_global(mood_slot, Value::Int(1));
}
let mut child = parent.fork(Mode::Normal);
{
let mut view = ContextView::new(&mut world, &mut parent);
view.set_global(mood_slot, Value::Int(2));
}
let view = ContextView::new(&mut world, &mut child);
assert_eq!(view.global(mood_slot), &Value::Int(1));
}
#[test]
fn sandbox_mode_writes_never_reach_world_reads_see_live_world() {
let program = sample_program();
let policy = mixed_policy(); let mut world = World::new(&program, &policy).expect("world builds");
let gold_slot = program.global_index("gold").expect("gold declared");
let shrine_id = program.find_path_target("shrine").expect("shrine exists");
world.set_global(gold_slot, Value::Int(7));
world.increment_visit(shrine_id);
world.increment_visit(shrine_id);
world.increment_visit(shrine_id);
assert_eq!(world.visit_count(shrine_id), 3);
let live = FlowLocal::new();
{
let mut sandboxed = live.fork(Mode::Sandbox);
{
let view = ContextView::new(&mut world, &mut sandboxed);
assert_eq!(view.global(gold_slot), &Value::Int(7));
assert_eq!(view.visit_count(shrine_id), 3);
}
{
let mut view = ContextView::new(&mut world, &mut sandboxed);
view.set_global(gold_slot, Value::Int(555));
assert_eq!(view.global(gold_slot), &Value::Int(555)); }
assert_eq!(world.global(gold_slot), &Value::Int(7));
{
let mut view = ContextView::new(&mut world, &mut sandboxed);
view.increment_visit(shrine_id);
assert_eq!(view.visit_count(shrine_id), 4); }
assert_eq!(world.visit_count(shrine_id), 3);
}
assert_eq!(world.global(gold_slot), &Value::Int(7));
assert_eq!(world.visit_count(shrine_id), 3);
}
}
#[cfg(test)]
mod save_load_tests {
use super::*;
use crate::link;
use crate::rng::FastRng;
use crate::story::{FallbackHandler, FlowInstance};
use crate::{load_state, save_state};
fn compile_for_flow(src: &str) -> (Program, Vec<Vec<brink_format::LineEntry>>) {
let out = brink_compiler::compile("t.ink", |p| {
if p == "t.ink" {
Ok(src.to_string())
} else {
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"no such include",
))
}
})
.expect("compile");
link(&out.data).expect("link")
}
#[test]
fn scoped_save_load_lands_each_unit_in_its_policy_layer() {
let (program, tables) = compile_for_flow(
"VAR gold = 0\n\
VAR silver = 0\n\
~ silver = 7\n\
-> shrine\n\
=== shrine ===\n\
~ gold = 5\n\
At the shrine.\n\
-> DONE\n\
=== reader ===\n\
{READ_COUNT(-> shrine)}\n\
-> DONE\n",
);
let mut overrides = BTreeMap::new();
overrides.insert("gold".to_owned(), Scope::Local);
overrides.insert("shrine".to_owned(), Scope::Local);
let policy = WorldPolicy {
default: Scope::World,
overrides,
turn_index: Scope::World,
rng: Scope::World,
};
let gold_slot = program.global_index("gold").expect("gold declared");
let silver_slot = program.global_index("silver").expect("silver declared");
let shrine_id = program.find_path_target("shrine").expect("shrine exists");
let mut world = World::new(&program, &policy).expect("world builds");
let mut local = FlowLocal::new();
let save = {
let (mut flow, _unused_default_world) = FlowInstance::new_at_root(&program);
let mut view = ContextView::new(&mut world, &mut local);
flow.drive_to_terminal::<FastRng>(&program, &tables, &mut view, &FallbackHandler, None)
.expect("drive succeeds");
save_state(&program, &view)
};
assert_eq!(save.globals.get("gold"), Some(&Value::Int(5)));
assert_eq!(save.globals.get("silver"), Some(&Value::Int(7)));
assert_eq!(
save.visits
.iter()
.find(|e| e.id == shrine_id)
.map(|e| e.count),
Some(1),
"shrine should have a captured visit entry"
);
let mut world2 = World::new(&program, &policy).expect("world builds");
let mut local2 = FlowLocal::new();
let report = {
let mut view2 = ContextView::new(&mut world2, &mut local2);
load_state(&program, &mut view2, &save)
};
assert!(report.unknown_globals.is_empty(), "clean load: {report:?}");
assert_eq!(
world2.global(gold_slot),
&Value::Int(0),
"gold is Local-scoped; World's own copy must stay untouched"
);
{
let view2 = ContextView::new(&mut world2, &mut local2);
assert_eq!(view2.global(gold_slot), &Value::Int(5));
}
assert_eq!(
world2.global(silver_slot),
&Value::Int(7),
"silver is World-scoped; must land directly in World"
);
assert_eq!(
world2.visit_count(shrine_id),
0,
"shrine is Local-scoped; World's own visit count must stay untouched"
);
{
let view2 = ContextView::new(&mut world2, &mut local2);
assert_eq!(view2.visit_count(shrine_id), 1);
}
}
}
#[cfg(test)]
mod subtree_scope_tests {
use super::*;
use crate::link;
use crate::rng::FastRng;
use crate::story::{FallbackHandler, FlowInstance, Line};
fn compile_for_flow(src: &str) -> (Program, Vec<Vec<brink_format::LineEntry>>) {
let out = brink_compiler::compile("t.ink", |p| {
if p == "t.ink" {
Ok(src.to_string())
} else {
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"no such include",
))
}
})
.expect("compile");
link(&out.data).expect("link")
}
fn story_with_stitch_and_sequences() -> (Program, Vec<Vec<brink_format::LineEntry>>) {
compile_for_flow(
"VAR gold = 0\n\
-> guard_talk\n\
=== guard_talk ===\n\
{ stopping: Halt! | Back again? }\n\
-> inner\n\
= inner\n\
{ stopping: A | B | C }\n\
-> DONE\n\
=== other_knot ===\n\
Other.\n\
-> DONE\n",
)
}
fn find_owned_sequence_id(program: &Program, scope_owner: DefinitionId) -> DefinitionId {
let mut found = None;
for (idx, container) in program.containers.iter().enumerate() {
#[expect(clippy::cast_possible_truncation, reason = "test fixture")]
let idx = idx as u32;
let owner = program.scope_ids[program.scope_table_idx(idx) as usize];
if owner == scope_owner
&& container
.counting_flags
.contains(brink_format::CountingFlags::VISITS)
{
assert!(
found.is_none(),
"expected exactly one VISITS-counted interior container owned by {scope_owner:?}"
);
found = Some(container.id);
}
}
found.expect("expected a VISITS-counted interior container")
}
#[test]
fn marked_local_knot_covers_its_interior_sequence_container() {
let (program, _tables) = story_with_stitch_and_sequences();
let guard_talk_id = program
.find_path_target("guard_talk")
.expect("guard_talk exists");
let other_knot_id = program
.find_path_target("other_knot")
.expect("other_knot exists");
let sequence_id = find_owned_sequence_id(&program, guard_talk_id);
let mut overrides = BTreeMap::new();
overrides.insert("guard_talk".to_owned(), Scope::Local);
let policy = WorldPolicy {
default: Scope::World,
overrides,
turn_index: Scope::World,
rng: Scope::World,
};
let resolved = ResolvedPolicy::resolve(&program, &policy).expect("resolves");
assert_eq!(resolved.scope_of_knot(guard_talk_id), Scope::Local);
assert_eq!(
resolved.scope_of_knot(sequence_id),
Scope::Local,
"the interior sequence container must inherit guard_talk's Local scope"
);
assert_eq!(resolved.scope_of_knot(other_knot_id), Scope::World);
}
#[test]
fn stitch_override_wins_over_enclosing_knot_for_its_own_subtree() {
let (program, _tables) = story_with_stitch_and_sequences();
let guard_talk_id = program
.find_path_target("guard_talk")
.expect("guard_talk exists");
let inner_id = program
.find_path_target("guard_talk.inner")
.expect("guard_talk.inner exists");
let guard_talk_sequence_id = find_owned_sequence_id(&program, guard_talk_id);
let inner_sequence_id = find_owned_sequence_id(&program, inner_id);
let mut overrides = BTreeMap::new();
overrides.insert("guard_talk".to_owned(), Scope::Local);
overrides.insert("guard_talk.inner".to_owned(), Scope::World);
let policy = WorldPolicy {
default: Scope::World,
overrides,
turn_index: Scope::World,
rng: Scope::World,
};
let resolved = ResolvedPolicy::resolve(&program, &policy).expect("resolves");
assert_eq!(resolved.scope_of_knot(guard_talk_id), Scope::Local);
assert_eq!(resolved.scope_of_knot(guard_talk_sequence_id), Scope::Local);
assert_eq!(
resolved.scope_of_knot(inner_id),
Scope::World,
"the stitch's own explicit override must win over its enclosing knot's"
);
assert_eq!(
resolved.scope_of_knot(inner_sequence_id),
Scope::World,
"the stitch's interior sequence must follow the stitch's own override, \
not the enclosing knot's"
);
let mut overrides2 = BTreeMap::new();
overrides2.insert("guard_talk".to_owned(), Scope::World);
overrides2.insert("guard_talk.inner".to_owned(), Scope::Local);
let policy2 = WorldPolicy {
default: Scope::World,
overrides: overrides2,
turn_index: Scope::World,
rng: Scope::World,
};
let resolved2 = ResolvedPolicy::resolve(&program, &policy2).expect("resolves");
assert_eq!(resolved2.scope_of_knot(guard_talk_id), Scope::World);
assert_eq!(
resolved2.scope_of_knot(guard_talk_sequence_id),
Scope::World
);
assert_eq!(resolved2.scope_of_knot(inner_id), Scope::Local);
assert_eq!(resolved2.scope_of_knot(inner_sequence_id), Scope::Local);
}
#[test]
fn all_world_default_still_takes_fast_path() {
let (program, _tables) = story_with_stitch_and_sequences();
let resolved =
ResolvedPolicy::resolve(&program, &WorldPolicy::default()).expect("resolves");
assert!(resolved.global_scopes.is_empty());
assert!(resolved.knot_scopes.is_empty());
let guard_talk_id = program
.find_path_target("guard_talk")
.expect("guard_talk exists");
assert_eq!(resolved.scope_of_knot(guard_talk_id), Scope::World);
}
#[test]
fn unknown_override_name_still_errors() {
let (program, _tables) = story_with_stitch_and_sequences();
let mut overrides = BTreeMap::new();
overrides.insert("guard_talk.nonexistent_stitch".to_owned(), Scope::Local);
let policy = WorldPolicy {
default: Scope::World,
overrides,
turn_index: Scope::World,
rng: Scope::World,
};
let err = ResolvedPolicy::resolve(&program, &policy).expect_err("must fail");
assert_eq!(
err,
PolicyError::UnknownName("guard_talk.nonexistent_stitch".to_owned())
);
}
#[test]
fn two_flows_over_shared_world_each_see_first_visit_text() {
let (program, tables) = compile_for_flow(
"VAR gold = 0\n\
-> guard_talk\n\
=== guard_talk ===\n\
{ stopping: Halt! | Back again? }\n\
-> DONE\n",
);
let mut overrides = BTreeMap::new();
overrides.insert("guard_talk".to_owned(), Scope::Local);
let policy = WorldPolicy {
default: Scope::World,
overrides,
turn_index: Scope::World,
rng: Scope::World,
};
let mut world = World::new(&program, &policy).expect("world builds");
let drive = |flow: &mut FlowInstance, view: &mut ContextView<'_>| -> String {
let lines = flow
.drive_to_terminal::<FastRng>(&program, &tables, view, &FallbackHandler, None)
.expect("drive succeeds");
assert!(
matches!(lines.last(), Some(Line::Done { .. })),
"expected Done, got {lines:?}"
);
lines.iter().map(Line::text).collect::<String>()
};
let (mut flow_a, _discarded_world_a) = FlowInstance::new_at_root(&program);
let mut local_a = FlowLocal::new();
let first_visit_a = {
let mut view_a = ContextView::new(&mut world, &mut local_a);
drive(&mut flow_a, &mut view_a)
};
let (mut flow_b, _discarded_world_b) = FlowInstance::new_at_root(&program);
let mut local_b = FlowLocal::new();
let first_visit_b = {
let mut view_b = ContextView::new(&mut world, &mut local_b);
drive(&mut flow_b, &mut view_b)
};
assert_eq!(
first_visit_a, first_visit_b,
"both flows' first encounter with guard_talk must produce identical \
(first-visit) text — each flow's visit count is independently local"
);
let second_visit_a = {
let mut view_a = ContextView::new(&mut world, &mut local_a);
flow_a
.choose_path_string(&program, &mut view_a, "guard_talk")
.expect("re-enter guard_talk");
drive(&mut flow_a, &mut view_a)
};
assert_ne!(
first_visit_a, second_visit_a,
"flow A's second encounter must progress past the first-visit branch"
);
let second_visit_b = {
let mut view_b = ContextView::new(&mut world, &mut local_b);
flow_b
.choose_path_string(&program, &mut view_b, "guard_talk")
.expect("re-enter guard_talk");
drive(&mut flow_b, &mut view_b)
};
assert_eq!(
second_visit_a, second_visit_b,
"flow B's second encounter must match flow A's second encounter — \
both progressed independently from the same (shared, untouched) \
World default"
);
let sequence_id = find_owned_sequence_id(&program, {
program
.find_path_target("guard_talk")
.expect("guard_talk exists")
});
assert_eq!(
world.visit_count(sequence_id),
0,
"World's own copy of the Local-scoped sequence's visit count must stay untouched"
);
}
}
#[cfg(test)]
mod compiled_defaults_tests {
use std::collections::BTreeMap;
use super::*;
use crate::link;
fn compile(src: &str) -> Program {
let out = brink_compiler::compile("t.ink", |p| {
if p == "t.ink" {
Ok(src.to_string())
} else {
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"no such include",
))
}
})
.expect("compile");
let (program, _line_tables) = link(&out.data).expect("link");
program
}
fn annotated_program() -> Program {
compile(
"VAR gold = 0\n\
#@local\n\
VAR mood = 0\n\
-> shrine\n\
=== shrine ===\n\
#@local\n\
At the shrine {&once|again}.\n\
= inner\n\
Deeper in.\n\
-> END\n\
=== cellar ===\n\
In the cellar.\n\
-> END\n",
)
}
#[test]
fn unannotated_program_keeps_the_fast_path() {
let program = compile("VAR gold = 0\nhello\n");
assert!(!program.has_local_defaults());
let resolved =
ResolvedPolicy::resolve(&program, &WorldPolicy::default()).expect("resolves");
assert!(resolved.global_scopes.is_empty());
assert!(resolved.knot_scopes.is_empty());
}
#[test]
fn compiled_local_var_seeds_the_base() {
let program = annotated_program();
assert!(program.has_local_defaults());
let resolved =
ResolvedPolicy::resolve(&program, &WorldPolicy::default()).expect("resolves");
let mood = program.global_index("mood").expect("mood declared");
let gold = program.global_index("gold").expect("gold declared");
assert_eq!(resolved.scope_of_global(mood), Scope::Local);
assert_eq!(resolved.scope_of_global(gold), Scope::World);
}
#[test]
fn compiled_local_knot_covers_its_subtree() {
let program = annotated_program();
let resolved =
ResolvedPolicy::resolve(&program, &WorldPolicy::default()).expect("resolves");
let shrine = program.find_path_target("shrine").expect("shrine exists");
let inner = program
.find_path_target("shrine.inner")
.expect("stitch exists");
let cellar = program.find_path_target("cellar").expect("cellar exists");
assert_eq!(resolved.scope_of_knot(shrine), Scope::Local);
assert_eq!(
resolved.scope_of_knot(inner),
Scope::Local,
"a #@local knot covers its stitches"
);
assert_eq!(resolved.scope_of_knot(cellar), Scope::World);
let interior = interior_containers_by_scope(&program);
let shrine_interior = interior.get(&shrine).cloned().unwrap_or_default();
assert!(
!shrine_interior.is_empty(),
"the {{&…}} sequence creates interior containers under shrine"
);
for id in shrine_interior {
assert_eq!(
resolved.scope_of_knot(id),
Scope::Local,
"interior container {id:?} inherits the knot's compiled scope"
);
}
}
#[test]
fn host_override_beats_the_compiled_bit() {
let program = annotated_program();
let mut overrides = BTreeMap::new();
overrides.insert("mood".to_owned(), Scope::World);
overrides.insert("shrine".to_owned(), Scope::World);
let policy = WorldPolicy {
default: Scope::World,
overrides,
turn_index: Scope::World,
rng: Scope::World,
};
let resolved = ResolvedPolicy::resolve(&program, &policy).expect("resolves");
let mood = program.global_index("mood").expect("mood declared");
let shrine = program.find_path_target("shrine").expect("shrine exists");
assert_eq!(
resolved.scope_of_global(mood),
Scope::World,
"host override wins over the compiled #@local bit"
);
assert_eq!(resolved.scope_of_knot(shrine), Scope::World);
}
#[test]
fn compiled_base_isolates_flows_without_host_policy() {
let program = annotated_program();
let mut world = World::new(&program, &WorldPolicy::default()).expect("world builds");
let mood = program.global_index("mood").expect("mood declared");
let gold = program.global_index("gold").expect("gold declared");
let mut local_a = FlowLocal::new();
let mut local_b = FlowLocal::new();
{
let mut view_a = ContextView::new(&mut world, &mut local_a);
view_a.set_global(mood, Value::Int(42));
view_a.set_global(gold, Value::Int(7));
}
{
let view_b = ContextView::new(&mut world, &mut local_b);
assert_eq!(view_b.global(mood), &Value::Int(0));
assert_eq!(view_b.global(gold), &Value::Int(7));
}
assert_eq!(world.global(mood), &Value::Int(0));
let shrine = program.find_path_target("shrine").expect("shrine exists");
{
let mut view_a = ContextView::new(&mut world, &mut local_a);
view_a.increment_visit(shrine);
assert_eq!(view_a.visit_count(shrine), 1);
}
{
let view_b = ContextView::new(&mut world, &mut local_b);
assert_eq!(view_b.visit_count(shrine), 0);
}
assert_eq!(world.visit_count(shrine), 0);
}
}