Skip to main content

brink_runtime/
world.rs

1//! Shared story state (`World`), the per-flow override layer (`FlowLocal`),
2//! and the routing view that composes them behind [`ContextAccess`].
3//!
4//! This is the F1.3 stage of the scoped-flow-state restructuring
5//! (`docs/scoped-flow-state-spec.md`): `World` replaces the old monolithic
6//! `Context` as the core mutable-state primitive. The [`ContextView`]
7//! routing view implements [`ContextAccess`] over `(&mut World, &mut
8//! FlowLocal)`.
9//!
10//! F2.1 adds the **policy** types ([`Scope`], [`WorldPolicy`],
11//! [`ResolvedPolicy`]) and their resolution against a [`Program`]'s symbol
12//! table.
13//!
14//! F2.2 gave `FlowLocal` flat override storage (plain maps/options — no
15//! `CoW` chain) and wired [`ContextView`] to route every [`ContextAccess`]
16//! op by consulting `World`'s [`ResolvedPolicy`] with **read-through**
17//! semantics: a `Local`-scoped unit reads its `FlowLocal` override if
18//! present, else falls back to `World`'s value; a `World`-scoped unit
19//! always goes straight to `World`. Writes to a `Local`-scoped unit land in
20//! `FlowLocal`; writes to a `World`-scoped unit land in `World`,
21//! immediately visible to every flow sharing it.
22//!
23//! F3.1 upgraded `FlowLocal`'s storage to a copy-on-write, frozen-base
24//! read-through **chain**: `FlowLocal` gains an optional [`Arc<FrozenLocal>`]
25//! base, an immutable snapshot of another `FlowLocal`'s overrides (captured
26//! via [`FlowLocal::freeze`]) that can itself chain to a further base. A read
27//! walks **own overrides → base (recursively) → [miss]**; a miss falls
28//! through to `World` exactly as before. Writes still land only in the
29//! flow's own top-layer overrides.
30//!
31//! **F3.2 (this stage)** adds **fork + sandbox mode + discard**: [`Mode`]
32//! (`Normal`/`Sandbox`), baked onto a `FlowLocal` at construction/fork time,
33//! and [`FlowLocal::fork`], which builds a child whose `base` is a frozen
34//! snapshot of the parent (via `freeze`) and whose own overrides start
35//! empty. `Normal` fork children route exactly like any other `FlowLocal` —
36//! by policy. `Sandbox` children are the side-effect-proof primitive
37//! watch/eval needs: `ContextView` treats **every** unit as `Local`
38//! regardless of policy, so the shared `World` is a read-only base — reads
39//! chain-read-through to `World`'s live value on a miss, but writes always
40//! land in the sandboxed flow's own overrides and never reach `World`.
41//! Discard is simply dropping the forked `FlowLocal`: since a `Sandbox`
42//! child's writes never touched `World` (and a `Normal` child's writes never
43//! touched its parent or `World` either — only its own top layer), there is
44//! nothing to unwind. A deferred `commit` seam (fold a fork's writes back
45//! into its parent) is documented but intentionally left unimplemented — see
46//! [`CommitError`] and [`commit`].
47//!
48//! No existing construction path calls `fork` or requests `Mode::Sandbox` —
49//! every flow the oracle corpus drives is `Mode::Normal` with `base: None`,
50//! so `ContextView` takes exactly the F3.1 branch on every op. The all-
51//! `World` policy (the default, and the only policy the oracle corpus
52//! exercises) takes the `World` branch on every op, so `ContextView` stays
53//! byte-identical to the F1.3 passthrough for every existing single-flow
54//! construction path.
55
56use std::collections::{BTreeMap, HashMap};
57use std::sync::Arc;
58
59use brink_format::{DefinitionId, Value};
60
61use crate::program::Program;
62use crate::rng::StoryRng;
63use crate::state::ContextAccess;
64
65// ── Policy ───────────────────────────────────────────────────────────────
66//
67// The scoped-flow-state model (`docs/scoped-flow-state-spec.md`, "The
68// policy") homes every unit of story-state — globals, visit/turn counts,
69// turn index, RNG — to either the shared `World` or a flow's private
70// `FlowLocal`. `WorldPolicy` is the host-facing, name-based declaration of
71// that split; `ResolvedPolicy` is the fast id/slot-based form the runtime
72// actually consults, built once at `World` creation.
73//
74// F2.1 introduces both shapes and resolution only. `ResolvedPolicy` is
75// stored on `World` but unread — F2.2 wires `ContextView` to consult it.
76
77/// Where a unit of story-state lives: the shared [`World`] or a flow's
78/// private [`FlowLocal`].
79///
80/// `World` is visible to every flow sharing that world immediately on
81/// write — the coordination path. `Local` is private to one flow; it
82/// persists for that flow's lifetime and only folds back into a parent via
83/// an explicit (currently unimplemented, F3) `commit`.
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
85pub enum Scope {
86    /// Shared across every flow over the world. This is the default —
87    /// matches today's single-`Context` behavior byte-for-byte.
88    #[default]
89    World,
90    /// Private to one flow.
91    Local,
92}
93
94/// Host-facing, name-based declaration of the world/local split.
95///
96/// Resolved once (via [`ResolvedPolicy::resolve`]) against a linked
97/// [`Program`]'s symbol table into a fast id/slot-based [`ResolvedPolicy`].
98/// Unlisted variables and knots/stitches fall back to `default`.
99///
100/// The all-`World` default (via [`WorldPolicy::default`]) is the
101/// degenerate, oracle-safety-anchoring policy: every unit homed to
102/// `World`, no overrides — identical to today's single-flow behavior.
103///
104/// **Name precedence:** a name in `overrides` is tried as a global variable
105/// first, then as a knot/stitch path (see [`ResolvedPolicy::resolve`]). If a
106/// name is (unusually) both a declared global VAR and a resolvable knot/
107/// stitch path, the override resolves against the **variable**, never the
108/// knot — the knot path is not consulted once a variable of the same name
109/// is found.
110///
111/// **Knot/stitch overrides are subtree-inclusive** (F6.1c —
112/// `docs/scoped-flow-state-spec.md`'s F6 AMENDMENT, ruling 3): a knot
113/// override covers the knot's own visit/turn count, every stitch nested
114/// directly under it, and every interior container (weave/sequence/choice
115/// container) nested anywhere under the knot or one of its stitches — not
116/// just the knot's own `DefinitionId`. This matters because ink's
117/// sequence/cycle/stopping machinery (`{ Halt! | Back again? }`) keys its
118/// counter off the *sequence's own* interior container id, not the
119/// enclosing knot's id; without subtree inclusion, a knot marked `Local`
120/// would leave those interior counters silently `World`-scoped.
121///
122/// **Most-specific override wins.** If both a knot and one of its stitches
123/// appear in `overrides` (e.g. knot `a` is `Local`, stitch `a.b` is
124/// `World`), every interior container nested under `a.b` resolves `World`;
125/// the rest of `a`'s subtree (the knot's own id, its other stitches, and
126/// any interior container not under `a.b`) resolves `Local`. A stitch's
127/// override always wins over its enclosing knot's for the stitch's own
128/// subtree, regardless of which name appears earlier in `overrides` (see
129/// [`ResolvedPolicy::resolve`] for how this is implemented).
130#[derive(Debug, Clone, Default)]
131pub struct WorldPolicy {
132    /// Scope for any variable or knot/stitch not named in `overrides`.
133    pub default: Scope,
134    /// Per-name exceptions to `default`, for global variables (matched
135    /// against `Program::global_index`'s name grammar) and knot/stitch
136    /// paths (matched against `Program::find_path_target`'s path
137    /// grammar). A name may appear in only one of the two — the resolver
138    /// tries variables first, then knot paths. Knot/stitch overrides are
139    /// subtree-inclusive with most-specific-wins precedence — see the type
140    /// docs above.
141    pub overrides: BTreeMap<String, Scope>,
142    /// Scope of the turn index (a single scalar field).
143    pub turn_index: Scope,
144    /// Scope of the RNG stream (`rng_seed` + `previous_random`, a single
145    /// scalar stream). See the spec's determinism caveat: a `World`-scoped
146    /// RNG interleaves draws from every flow sharing the world by
147    /// execution order.
148    pub rng: Scope,
149}
150
151/// Errors resolving a [`WorldPolicy`] against a [`Program`]'s symbol table.
152///
153/// Resolution happens once, at `World` creation — an unknown name here is
154/// a host configuration error, not a runtime one.
155#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
156pub enum PolicyError {
157    /// A name in `WorldPolicy::overrides` matched neither a declared
158    /// global variable nor a resolvable knot/stitch path.
159    #[error("unknown variable or knot/stitch in world policy overrides: {0}")]
160    UnknownName(String),
161}
162
163/// Fast, id/slot-based resolution of a [`WorldPolicy`] against a specific
164/// [`Program`]. Built once at `World` creation via
165/// [`ResolvedPolicy::resolve`]; consulted on every state access (from F2.2
166/// on) with O(1) lookups — no string matching on the hot path.
167#[derive(Debug, Clone)]
168pub struct ResolvedPolicy {
169    /// Default scope for globals/knots not otherwise listed.
170    default: Scope,
171    /// Per-slot scope for every global variable, dense (length ==
172    /// `Program::global_count()`). Populated with `default` for slots with
173    /// no override, so lookups never need a fallback branch.
174    global_scopes: Vec<Scope>,
175    /// Non-default scope for a knot/stitch (or an interior weave/sequence/
176    /// choice container nested under one), keyed by its defining
177    /// `DefinitionId` (the same id `ContextAccess::visit_count` and friends
178    /// are called with — e.g. `vm.rs`'s `handle_sequence` keys a stopping/
179    /// cycle sequence's counter off the *sequence's own* interior container
180    /// id, not its enclosing knot's).
181    ///
182    /// **Subtree-inclusive (F6.1c):** an override on a knot/stitch name is
183    /// expanded at resolve time (see [`resolve`](Self::resolve)) to cover
184    /// its own id, every stitch nested directly under it (if it's a knot),
185    /// and every interior container nested anywhere under it or one of its
186    /// stitches — not just the literal `DefinitionId` the override name
187    /// resolved to. Only exceptions to `default` are stored — sparse, since
188    /// most programs have far more knots/interior containers than
189    /// overrides.
190    knot_scopes: HashMap<DefinitionId, Scope>,
191    /// Scope of the turn index.
192    turn_index: Scope,
193    /// Scope of the RNG stream.
194    rng: Scope,
195}
196
197impl ResolvedPolicy {
198    /// The all-`World` resolved policy — no name lookups needed. This is
199    /// the fast path for [`WorldPolicy::default()`] and the only policy
200    /// exercised by the oracle-anchored single-flow path.
201    #[must_use]
202    pub fn all_world() -> Self {
203        Self {
204            default: Scope::World,
205            global_scopes: Vec::new(),
206            knot_scopes: HashMap::new(),
207            turn_index: Scope::World,
208            rng: Scope::World,
209        }
210    }
211
212    /// Resolve a host-facing [`WorldPolicy`] against a linked `Program`'s
213    /// symbol table.
214    ///
215    /// Variable names are resolved via [`Program::global_index`]; knot/
216    /// stitch paths via `Program`'s path-to-`DefinitionId` resolution
217    /// (the same table `find_address`/`find_path_target` use). A name is
218    /// tried as a variable first, then as a knot/stitch path; a name
219    /// matching neither is a [`PolicyError::UnknownName`].
220    ///
221    /// **Subtree expansion (F6.1c).** Every knot/stitch override is
222    /// expanded here, once, into every `DefinitionId` in its definition
223    /// subtree — see [`expand_knot_scope`] for the containment mechanism
224    /// (`Program::scope_ids`, the nearest-enclosing-scope table every
225    /// interior container carries, plus `Program::address_by_path`'s
226    /// dotted knot/stitch grammar for the one-level knot→stitch link that
227    /// `scope_ids` alone doesn't carry). `overrides` is a `BTreeMap`, so
228    /// this loop's iteration order is deterministic (sorted by name) — and
229    /// because a knot's name is always a proper prefix of (and therefore
230    /// sorts lexicographically before) any of its stitches' names, a knot
231    /// override's subtree expansion always runs *before* a same-subtree
232    /// stitch override in this same pass, so the stitch override's own
233    /// `insert`s (which land later) win — implementing "most-specific
234    /// override wins" as a natural consequence of processing order, no
235    /// separate precedence pass needed.
236    ///
237    /// **Compiled base layer.** Before host overrides apply, `resolve`
238    /// seeds scopes from the `Program`'s compiled `#@local` defaults
239    /// (`docs/directive-annotations-spec.md`): globals the compiler
240    /// marked flow-private seed `Local`, and every `#@local` knot/stitch
241    /// expands over its subtree exactly like a host override would.
242    /// Host overrides then layer on top — `base ⊕ host-overrides` — so a
243    /// host name always beats the compiled bit for that name's subtree.
244    ///
245    /// The all-`World` default (empty `overrides`, no compiled `#@local`
246    /// bits) resolves without any name lookups (see
247    /// [`all_world`](Self::all_world)) — this is the fast path every
248    /// unannotated single-flow program takes.
249    pub fn resolve(program: &Program, policy: &WorldPolicy) -> Result<Self, PolicyError> {
250        if policy.overrides.is_empty()
251            && policy.default == Scope::World
252            && policy.turn_index == Scope::World
253            && policy.rng == Scope::World
254            && !program.has_local_defaults()
255        {
256            return Ok(Self::all_world());
257        }
258
259        // Seed globals from the compiled base: `#@local` beats the host
260        // default; everything unmarked follows the host default.
261        let mut global_scopes: Vec<Scope> = (0..program.global_count())
262            .map(|slot| {
263                if program.global_is_local(slot) {
264                    Scope::Local
265                } else {
266                    policy.default
267                }
268            })
269            .collect();
270        let mut knot_scopes = HashMap::new();
271        let interior_by_scope = interior_containers_by_scope(program);
272
273        // Seed knots/stitches from the compiled base. The list is sorted
274        // by path at link time, so a `#@local` knot expands before any of
275        // its own `#@local` stitches — same ordering argument as the
276        // override loop below.
277        for (path, id) in program.local_scope_defaults() {
278            expand_knot_scope(
279                program,
280                &interior_by_scope,
281                &mut knot_scopes,
282                path,
283                *id,
284                Scope::Local,
285            );
286        }
287
288        // `overrides` is a `BTreeMap`, so iteration order is deterministic
289        // (sorted by name) — resolution never depends on hash-map order.
290        for (name, &scope) in &policy.overrides {
291            if let Some(slot) = program.global_index(name) {
292                global_scopes[slot as usize] = scope;
293            } else if let Some(id) = program.find_path_target(name) {
294                expand_knot_scope(
295                    program,
296                    &interior_by_scope,
297                    &mut knot_scopes,
298                    name,
299                    id,
300                    scope,
301                );
302            } else {
303                return Err(PolicyError::UnknownName(name.clone()));
304            }
305        }
306
307        Ok(Self {
308            default: policy.default,
309            global_scopes,
310            knot_scopes,
311            turn_index: policy.turn_index,
312            rng: policy.rng,
313        })
314    }
315
316    /// Scope of a global variable by slot index.
317    #[must_use]
318    pub fn scope_of_global(&self, slot: u32) -> Scope {
319        self.global_scopes
320            .get(slot as usize)
321            .copied()
322            .unwrap_or(self.default)
323    }
324
325    /// Scope of a knot/stitch — or of an interior weave/sequence/choice
326    /// container nested under one — by its defining `DefinitionId`. See the
327    /// `knot_scopes` field docs and [`resolve`](Self::resolve) for how a
328    /// knot/stitch override is expanded, at resolve time, to cover every id
329    /// in its definition subtree.
330    #[must_use]
331    pub fn scope_of_knot(&self, id: DefinitionId) -> Scope {
332        self.knot_scopes.get(&id).copied().unwrap_or(self.default)
333    }
334
335    /// Scope of the turn index.
336    #[must_use]
337    pub fn turn_index_scope(&self) -> Scope {
338        self.turn_index
339    }
340
341    /// Scope of the RNG stream.
342    #[must_use]
343    pub fn rng_scope(&self) -> Scope {
344        self.rng
345    }
346}
347
348// ── Subtree-inclusive knot scope (F6.1c) ────────────────────────────────
349//
350// Containment mechanism, verified against a compiled `Program` (see the
351// `subtree_scope_tests` module below and the investigation in the F6.1c
352// build log — not restated here):
353//
354// - `ContainerDef::scope_id` (preserved on `Program` as the parallel
355//   `scope_ids`/`scope_table_idx` tables) gives every container — knot,
356//   stitch, root, or an anonymous interior weave/sequence/choice
357//   container, at any nesting depth — the `DefinitionId` of its *nearest
358//   enclosing* knot/stitch/root scope, correctly propagated through
359//   arbitrarily deep nesting by the codegen's recursive walk. A container
360//   is itself a scope owner (a knot, stitch, or root) exactly when its own
361//   `scope_id` equals its own id — self-scoped, no parent. This gives an
362//   exact, structural (not name-based) map from any interior container to
363//   its owning knot/stitch: `interior_containers_by_scope`, below.
364// - `scope_id` does *not* link a stitch to its enclosing knot (a stitch is
365//   self-scoped, by the same rule above) — ink only nests stitches one
366//   level under a knot, and that one link has to come from
367//   `Program::address_by_path`'s dotted qualified-path grammar (the same
368//   table `find_address`/`find_path_target` already use): a knot named `N`
369//   knows its direct stitches are exactly the `address_by_path` entries
370//   `"N.<segment>"` with no further `.` in `<segment>`, whose target is
371//   itself a scope owner (ruling out an author-labeled gather directly in
372//   the knot, which shares the same two-segment path shape but is *not*
373//   self-scoped).
374//
375// Both of these are compile-time/link-time structural facts already
376// present on `Program` — no id arithmetic or heuristic string matching on
377// unstructured names.
378
379/// Group every non-scope-owning ("interior") container's own id by the
380/// `DefinitionId` of its nearest enclosing knot/stitch/root scope.
381///
382/// Built once per non-fast-path [`ResolvedPolicy::resolve`] call — this is
383/// resolve-time bookkeeping, not a hot-path lookup (the all-`World` fast
384/// path never calls this at all).
385fn interior_containers_by_scope(program: &Program) -> HashMap<DefinitionId, Vec<DefinitionId>> {
386    let mut by_scope: HashMap<DefinitionId, Vec<DefinitionId>> = HashMap::new();
387    for (idx, container) in program.containers.iter().enumerate() {
388        #[expect(
389            clippy::cast_possible_truncation,
390            reason = "container count fits in u32"
391        )]
392        let idx = idx as u32;
393        let owner = program.scope_ids[program.scope_table_idx(idx) as usize];
394        if owner != container.id {
395            by_scope.entry(owner).or_default().push(container.id);
396        }
397    }
398    by_scope
399}
400
401/// Apply `scope` to `id` and every interior container `interior_by_scope`
402/// says is nested directly under it (i.e. `id`'s own subtree, one scope
403/// level — not a recursive walk, since ink knots/stitches never nest
404/// containers whose `scope_id` chain needs more than one enclosing-scope
405/// hop to resolve; see `interior_containers_by_scope`).
406fn apply_scope_to_subtree(
407    interior_by_scope: &HashMap<DefinitionId, Vec<DefinitionId>>,
408    knot_scopes: &mut HashMap<DefinitionId, Scope>,
409    id: DefinitionId,
410    scope: Scope,
411) {
412    knot_scopes.insert(id, scope);
413    if let Some(interior) = interior_by_scope.get(&id) {
414        for &child_id in interior {
415            knot_scopes.insert(child_id, scope);
416        }
417    }
418}
419
420/// Expand a single `WorldPolicy::overrides` knot/stitch entry (`name` →
421/// `id`, already resolved via `Program::find_path_target`) into every
422/// `DefinitionId` in its definition subtree, writing `scope` for each into
423/// `knot_scopes`.
424///
425/// Covers: `id`'s own subtree (its own id plus its direct interior
426/// containers), then — since a stitch override's own call to this function
427/// already covers everything a stitch can own, and ink never nests a
428/// stitch under another stitch — cascades once to `name`'s direct child
429/// stitches (found via `address_by_path`'s dotted grammar, see the module
430/// docs above) and covers each of *their* subtrees too. A child stitch
431/// that itself has a more specific override is still safe to cascade into
432/// here: `resolve`'s `BTreeMap` iteration order guarantees the stitch's own
433/// (later, more specific) entry is processed after `name`'s and overwrites
434/// whatever this cascade wrote — see `resolve`'s docs.
435fn expand_knot_scope(
436    program: &Program,
437    interior_by_scope: &HashMap<DefinitionId, Vec<DefinitionId>>,
438    knot_scopes: &mut HashMap<DefinitionId, Scope>,
439    name: &str,
440    id: DefinitionId,
441    scope: Scope,
442) {
443    apply_scope_to_subtree(interior_by_scope, knot_scopes, id, scope);
444
445    let prefix = format!("{name}.");
446    for (path, target) in &program.address_by_path {
447        let Some(rest) = path.strip_prefix(prefix.as_str()) else {
448            continue;
449        };
450        if rest.is_empty() || rest.contains('.') {
451            continue; // Not a direct one-segment child of `name`.
452        }
453        if target.byte_offset != 0 {
454            continue; // Not a container's own primary address.
455        }
456        // Confirm the target is itself a scope-owning container (a real
457        // stitch), not an author-labeled gather directly in the knot that
458        // happens to share the same two-segment path shape.
459        let owner = program.scope_ids[program.scope_table_idx(target.container_idx) as usize];
460        if owner != target.id {
461            continue;
462        }
463        apply_scope_to_subtree(interior_by_scope, knot_scopes, target.id, scope);
464    }
465}
466
467/// Shared game state that lives above individual flows.
468///
469/// Holds globals, visit/turn tracking, and RNG state. This is the natural
470/// serialization boundary for save/load (deferred).
471///
472/// Multiple [`FlowInstance`](crate::FlowInstance)s can share a single
473/// `World` (matching inklecate's semantics where flow writes are
474/// immediately visible to other flows), or each flow can hold its own
475/// cloned `World` if the consumer wants fork/branch/rollback semantics.
476/// The runtime's step functions take `&mut World` (or any
477/// `&mut impl ContextAccess`) without prescribing where it lives.
478#[derive(Debug, Clone)]
479pub struct World {
480    pub globals: Vec<Value>,
481    pub visit_counts: HashMap<DefinitionId, u32>,
482    pub turn_counts: HashMap<DefinitionId, u32>,
483    pub turn_index: u32,
484    pub rng_seed: i32,
485    pub previous_random: i32,
486    /// The resolved world/local scoping policy for this world.
487    ///
488    /// Boxed so `World` (cloned per-flow-spawn and stored inline in several
489    /// call sites and enums across the crate graph) doesn't balloon in size
490    /// for consumers that never touch policy — `ResolvedPolicy` carries a
491    /// per-slot `Vec` and a `HashMap` that dwarf `World`'s other fields.
492    ///
493    /// **Consulted by [`ContextView`] (F2.2 on)** to route every
494    /// [`ContextAccess`] op between `World` and `FlowLocal`. Every
495    /// construction path that predates policy (`World::from_globals`,
496    /// `FlowInstance::new_at*`, `Story::new`) resolves
497    /// [`WorldPolicy::default()`] (all-`World`), so those paths route every
498    /// op straight to `World` — unchanged from F1.3.
499    policy: Box<ResolvedPolicy>,
500}
501
502impl World {
503    /// Create a fresh `World` for `program`, resolving `policy` against the
504    /// program's symbol table.
505    ///
506    /// Globals are initialized from the program's declared defaults; visit
507    /// counts, turn counts, turn index, and RNG state start zeroed —
508    /// identical to `FlowInstance::new_at`'s inline construction. Fails if
509    /// `policy` names a variable or knot/stitch the program doesn't
510    /// declare ([`PolicyError::UnknownName`]).
511    ///
512    /// [`WorldPolicy::default()`] (all-`World`) always resolves — see
513    /// [`ResolvedPolicy::all_world`] — so passing it here can't produce a
514    /// `PolicyError`.
515    pub fn new(program: &Program, policy: &WorldPolicy) -> Result<Self, PolicyError> {
516        let resolved = ResolvedPolicy::resolve(program, policy)?;
517        Ok(Self::from_globals(program.global_defaults(), resolved))
518    }
519
520    /// Build a `World` from an explicit globals vector and an
521    /// already-resolved policy. Used by [`crate::FlowInstance::new_at`] (whose
522    /// signature predates policy and can't take a `Result`) to construct
523    /// the all-`World` default without re-deriving it from a `Program` each
524    /// time.
525    pub(crate) fn from_globals(globals: Vec<Value>, policy: ResolvedPolicy) -> Self {
526        Self {
527            globals,
528            visit_counts: HashMap::new(),
529            turn_counts: HashMap::new(),
530            turn_index: 0,
531            rng_seed: 0,
532            previous_random: 0,
533            policy: Box::new(policy),
534        }
535    }
536
537    /// Construct a `World` directly from its field values, with the
538    /// all-`World` policy. Only for test fixtures that need to hand-build a
539    /// `World` without a `Program` (e.g. `bevy-brink`'s commit-merge
540    /// tests) — production code should go through [`World::new`].
541    #[cfg(feature = "testing")]
542    #[must_use]
543    pub fn new_for_testing(
544        globals: Vec<Value>,
545        visit_counts: HashMap<DefinitionId, u32>,
546        turn_counts: HashMap<DefinitionId, u32>,
547        turn_index: u32,
548        rng_seed: i32,
549        previous_random: i32,
550    ) -> Self {
551        Self {
552            globals,
553            visit_counts,
554            turn_counts,
555            turn_index,
556            rng_seed,
557            previous_random,
558            policy: Box::new(ResolvedPolicy::all_world()),
559        }
560    }
561}
562
563/// A flow-local override of the shared RNG stream (`rng_seed` +
564/// `previous_random`), the two scalars [`WorldPolicy::rng`] scopes as a
565/// single unit.
566#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
567pub struct LocalRng {
568    pub seed: i32,
569    pub previous_random: i32,
570}
571
572/// An immutable snapshot of a [`FlowLocal`]'s override layer, frozen via
573/// [`FlowLocal::freeze`].
574///
575/// `FrozenLocal` chains: its own `base` is whatever the source `FlowLocal`'s
576/// base was at freeze time, so a chain of forks ([`FlowLocal::fork`]) can
577/// walk arbitrarily far back through frozen ancestors. Cloning a
578/// `FrozenLocal` reference is cheap — callers hold it behind an [`Arc`].
579#[derive(Debug, Clone, Default)]
580pub struct FrozenLocal {
581    /// Overridden values for globals `ResolvedPolicy` homes to `Local`,
582    /// keyed by slot index.
583    globals: BTreeMap<u32, Value>,
584    /// Overridden visit counts for knots/stitches homed to `Local`.
585    visit_counts: BTreeMap<DefinitionId, u32>,
586    /// Overridden turn counts for knots homed to `Local`.
587    turn_counts: BTreeMap<DefinitionId, u32>,
588    /// Overridden turn index, when `turn_index_scope() == Local`.
589    turn_index: Option<u32>,
590    /// Overridden RNG stream, when `rng_scope() == Local`.
591    rng: Option<LocalRng>,
592    /// The next link in the chain, if this snapshot's source `FlowLocal`
593    /// itself had a base at freeze time.
594    base: Option<Arc<FrozenLocal>>,
595}
596
597impl FrozenLocal {
598    /// Chain-lookup a global override: this snapshot's own overrides, else
599    /// recurse into `base`. Returns `None` on a miss all the way down the
600    /// chain — the caller falls through to `World`.
601    fn chain_get_global(&self, idx: u32) -> Option<&Value> {
602        self.globals
603            .get(&idx)
604            .or_else(|| self.base.as_deref().and_then(|b| b.chain_get_global(idx)))
605    }
606
607    /// Chain-lookup a visit-count override.
608    fn chain_get_visit_count(&self, id: DefinitionId) -> Option<u32> {
609        self.visit_counts.get(&id).copied().or_else(|| {
610            self.base
611                .as_deref()
612                .and_then(|b| b.chain_get_visit_count(id))
613        })
614    }
615
616    /// Chain-lookup a turn-count override.
617    fn chain_get_turn_count(&self, id: DefinitionId) -> Option<u32> {
618        self.turn_counts.get(&id).copied().or_else(|| {
619            self.base
620                .as_deref()
621                .and_then(|b| b.chain_get_turn_count(id))
622        })
623    }
624
625    /// Chain-lookup the overridden turn index.
626    fn chain_get_turn_index(&self) -> Option<u32> {
627        self.turn_index.or_else(|| {
628            self.base
629                .as_deref()
630                .and_then(FrozenLocal::chain_get_turn_index)
631        })
632    }
633
634    /// Chain-lookup the overridden RNG stream.
635    fn chain_get_rng(&self) -> Option<LocalRng> {
636        self.rng
637            .or_else(|| self.base.as_deref().and_then(FrozenLocal::chain_get_rng))
638    }
639}
640
641/// Execution mode of a [`FlowLocal`], baked in at construction/fork time and
642/// read by [`ContextView`] to decide how it routes every unit.
643///
644/// `Mode` is orthogonal to [`WorldPolicy`]/[`ResolvedPolicy`]: policy homes a
645/// *unit* (a global, a knot's visit count, …) to `World` or `Local`; `Mode`
646/// decides, for *this flow*, whether that homing is honored at all.
647#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
648pub enum Mode {
649    /// Route every unit by policy, exactly as [`ContextView`]'s F2.2/F3.1
650    /// docs describe: `World`-scoped units go straight to `World`,
651    /// `Local`-scoped units chain-read-through/write to `FlowLocal`. Every
652    /// construction path before F3.2 produces `Mode::Normal` — this is what
653    /// keeps the oracle corpus byte-identical.
654    #[default]
655    Normal,
656    /// Treat **every** unit as `Local`, regardless of policy: the shared
657    /// `World` becomes a read-only base for this flow. Reads still
658    /// chain-read-through to `World`'s current value on a total miss (so a
659    /// sandboxed flow sees live world state), but writes always land in
660    /// this flow's own top-layer overrides — `World` (and any `Normal`
661    /// ancestor) is never mutated. Combined with [`FlowLocal::fork`]'s
662    /// frozen base, this is the side-effect-proof primitive watch/eval
663    /// needs: run a flow against current state, observe its output, then
664    /// discard it (drop) with the shared world untouched.
665    Sandbox,
666}
667
668/// Per-flow override layer over the shared [`World`].
669///
670/// **F3.1: copy-on-write, frozen-base read-through chain.** Each field is a
671/// plain map/option holding this flow's own overrides for units
672/// [`ResolvedPolicy`] homes to [`Scope::Local`] (or, in [`Mode::Sandbox`],
673/// *every* unit — see [`Mode`]), plus an optional `base`: an immutable
674/// [`FrozenLocal`] snapshot (see [`FlowLocal::freeze`]) of another
675/// `FlowLocal`, captured at some earlier point. A read walks **own
676/// overrides → base (recursively) → [miss]**; [`ContextView`] treats a miss
677/// as "not in the local chain" and falls through to `World`, exactly as in
678/// F2.2. Writes always land in the flow's own top-layer overrides — never
679/// in `base`, which is immutable by construction.
680///
681/// A fresh `FlowLocal` (via `Default`/[`FlowLocal::new`]) has empty
682/// overrides, `base: None`, and `mode: Mode::Normal`, so it contributes no
683/// reads and every access falls through to `World` — this is what keeps the
684/// all-`World` policy (and every construction path that doesn't call
685/// [`fork`](Self::fork)) byte-identical to the F2.2 flat-storage behavior.
686/// [`FlowLocal::fork`] (F3.2) is what actually populates a child's `base` by
687/// freezing its parent, and what bakes in a non-`Normal` `mode`.
688///
689/// [`ContextView`] (below) is what actually consults these maps; see its
690/// docs for the read-through/copy-on-write-increment/mode semantics.
691#[derive(Debug, Clone, Default)]
692pub struct FlowLocal {
693    /// Overridden values for globals `ResolvedPolicy` homes to `Local`,
694    /// keyed by slot index.
695    globals: BTreeMap<u32, Value>,
696    /// Overridden visit counts for knots/stitches homed to `Local`.
697    visit_counts: BTreeMap<DefinitionId, u32>,
698    /// Overridden turn counts for knots homed to `Local`.
699    turn_counts: BTreeMap<DefinitionId, u32>,
700    /// Overridden turn index, when `turn_index_scope() == Local`.
701    turn_index: Option<u32>,
702    /// Overridden RNG stream, when `rng_scope() == Local`.
703    rng: Option<LocalRng>,
704    /// Frozen snapshot of an earlier `FlowLocal`'s overrides, read *after*
705    /// this layer's own overrides on a miss. Populated by [`FlowLocal::fork`];
706    /// `None` for every construction path that doesn't fork.
707    base: Option<Arc<FrozenLocal>>,
708    /// This flow's execution mode — see [`Mode`]. Baked in at construction
709    /// (`Mode::Normal`, the `Default`) or at [`FlowLocal::fork`] time.
710    mode: Mode,
711}
712
713impl FlowLocal {
714    /// Construct an empty flow-local layer — overrides nothing and has no
715    /// base, so every access routes through to `World`.
716    #[must_use]
717    pub fn new() -> Self {
718        Self::default()
719    }
720
721    /// Freeze this `FlowLocal`'s current state into an immutable
722    /// [`FrozenLocal`] snapshot, suitable for use as another `FlowLocal`'s
723    /// `base`.
724    ///
725    /// Captures the override maps (cloned — cheap, since only `Local`-scoped
726    /// units are ever present) and cheap-clones the current `base` `Arc` so
727    /// the new snapshot chains to the same ancestry this `FlowLocal` had.
728    ///
729    /// Called by [`FlowLocal::fork`] to snapshot a parent into a child's
730    /// `base`.
731    #[must_use]
732    fn freeze(&self) -> Arc<FrozenLocal> {
733        Arc::new(FrozenLocal {
734            globals: self.globals.clone(),
735            visit_counts: self.visit_counts.clone(),
736            turn_counts: self.turn_counts.clone(),
737            turn_index: self.turn_index,
738            rng: self.rng,
739            base: self.base.clone(),
740        })
741    }
742
743    /// Fork a child `FlowLocal` from this one.
744    ///
745    /// The child's `base` is a frozen, point-in-time snapshot of `self` (via
746    /// [`freeze`](Self::freeze)) — an `O(1)`-ish operation that clones this
747    /// flow's own (small) override maps and `Arc`-bumps the rest of the
748    /// ancestry chain, never a full `World` copy. The child's own override
749    /// layer starts empty, and it runs in `mode` for its lifetime (`Mode` is
750    /// baked in here, not mutable afterward).
751    ///
752    /// Because the base is frozen, later mutations to `self` (the parent)
753    /// are **not** visible to the child — the child sees the parent exactly
754    /// as it was at fork time. Symmetrically, nothing the child does is ever
755    /// visible to `self` or `World`: writes land only in the child's own top
756    /// layer (see [`Mode`] for how `Sandbox` additionally diverts
757    /// `World`-scoped writes there too). That makes **discard** trivial —
758    /// dropping the returned `FlowLocal` is the entire discard operation, no
759    /// unwinding required. Folding a child's writes back into `self` instead
760    /// of discarding them is the deferred `commit` seam — see [`CommitError`]
761    /// and [`commit`].
762    #[must_use]
763    pub fn fork(&self, mode: Mode) -> FlowLocal {
764        FlowLocal {
765            base: Some(self.freeze()),
766            mode,
767            ..FlowLocal::new()
768        }
769    }
770
771    /// Chain-lookup a global override: own overrides → `base` (recursively)
772    /// → `None` on a total miss, which [`ContextView`] treats as "fall
773    /// through to `World`".
774    fn chain_get_global(&self, idx: u32) -> Option<&Value> {
775        self.globals
776            .get(&idx)
777            .or_else(|| self.base.as_deref().and_then(|b| b.chain_get_global(idx)))
778    }
779
780    /// Chain-lookup a visit-count override.
781    fn chain_get_visit_count(&self, id: DefinitionId) -> Option<u32> {
782        self.visit_counts.get(&id).copied().or_else(|| {
783            self.base
784                .as_deref()
785                .and_then(|b| b.chain_get_visit_count(id))
786        })
787    }
788
789    /// Chain-lookup a turn-count override.
790    fn chain_get_turn_count(&self, id: DefinitionId) -> Option<u32> {
791        self.turn_counts.get(&id).copied().or_else(|| {
792            self.base
793                .as_deref()
794                .and_then(|b| b.chain_get_turn_count(id))
795        })
796    }
797
798    /// Chain-lookup the overridden turn index.
799    fn chain_get_turn_index(&self) -> Option<u32> {
800        self.turn_index.or_else(|| {
801            self.base
802                .as_deref()
803                .and_then(FrozenLocal::chain_get_turn_index)
804        })
805    }
806
807    /// Chain-lookup the overridden RNG stream.
808    fn chain_get_rng(&self) -> Option<LocalRng> {
809        self.rng
810            .or_else(|| self.base.as_deref().and_then(FrozenLocal::chain_get_rng))
811    }
812}
813
814/// Errors from [`commit`].
815///
816/// A single variant today: `commit` is a documented, deferred seam (see
817/// `docs/scoped-flow-state-spec.md`, "Write-back is determined by scope, not
818/// a separate knob") — this release ships fork + discard only.
819#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
820pub enum CommitError {
821    /// Folding a forked child's own-layer overrides back into its parent is
822    /// deferred past this release. Fork's only supported terminal operation
823    /// today is **discard** (drop the child); `commit` always returns this.
824    #[error(
825        "commit is not implemented in this release; fork's only supported terminal operation is discard (drop the child)"
826    )]
827    NotImplemented,
828}
829
830/// Fold a forked child's own-layer overrides back into its parent's,
831/// making the child's writes visible through `parent` (and, transitively,
832/// anything `parent` itself chains to or is later written through).
833///
834/// **This is a deferred seam, not implemented in this release.** It exists
835/// so the shape of the eventual write-back API is fixed and callers can
836/// write code against it now (always getting `CommitError::NotImplemented`
837/// back) rather than the API appearing later with no forward-compatible
838/// slot. Per the spec, only a **fork** ever commits — a root flow has no
839/// parent to fold into, and its `Local`-scoped writes already persist for
840/// its own lifetime; `World`-scoped writes already escape live, with no
841/// "make it back" step needed.
842///
843/// Intended semantics, when implemented: walk `child`'s own top-layer
844/// overrides (globals, visit counts, turn counts, turn index, RNG) and
845/// apply each onto `parent`'s own top layer, as if `child`'s writes had
846/// been made directly against `parent` — last-write-wins per unit, since a
847/// fork is single-writer for its whole lifetime (no concurrent-write
848/// conflict is possible, so no merge policy is needed). Commit never
849/// touches `World` directly: a folded `Local`-scoped write still only
850/// lands in `parent`'s overrides, reaching `World` only if `parent` is
851/// itself later written through a `World`-scoped op or committed further up
852/// the chain. `Sandbox`-mode writes are exactly what this would fold
853/// back — commit is what would turn a sandboxed probe into a real,
854/// persisted mutation, for a caller that chooses to call it instead of
855/// dropping the child.
856///
857/// # Errors
858///
859/// Always returns [`CommitError::NotImplemented`] in this release.
860pub fn commit(_child: FlowLocal, _parent: &mut FlowLocal) -> Result<(), CommitError> {
861    Err(CommitError::NotImplemented)
862}
863
864/// Routing view implementing [`ContextAccess`] over `(&mut World, &mut
865/// FlowLocal)`.
866///
867/// This is what the VM's drive path receives as its `impl ContextAccess`.
868/// Every op computes an **effective scope** for its unit — see
869/// [`ContextView::effective_scope`] — and then routes exactly as before:
870///
871/// - **`World`-scoped**: always routes straight to `World` — reads and
872///   writes are immediately visible to every flow sharing that `World`.
873/// - **`Local`-scoped, read**: **chain read-through** — walks the
874///   `FlowLocal`'s own overrides, then its frozen `base` (recursively, see
875///   [`FlowLocal::chain_get_global`] and friends), then falls back to
876///   `World`'s current value on a total miss (so a flow that has never
877///   written a Local unit, nor inherited one from a base, sees the shared
878///   default until its first local write).
879/// - **`Local`-scoped, write**: lands in the `FlowLocal`'s own top-layer
880///   overrides only; `World` (and any frozen `base`) is untouched.
881/// - **`Local`-scoped, increment** (`increment_visit`,
882///   `increment_turn_index`): copy-on-write from the *chain read-through*
883///   value — read the current value (own override, else base chain, else
884///   World fallback), add one, store the result as the new top-layer
885///   override. This is what makes a flow's first local increment start
886///   from the chain's (or World's) count rather than 0.
887///
888/// **The effective scope, not the raw policy scope, drives all of the
889/// above.** In [`Mode::Normal`] (every construction path before F3.2, and
890/// every non-forked flow today) the effective scope of a unit *is* its
891/// `ResolvedPolicy` scope — unchanged from F2.2/F3.1. In [`Mode::Sandbox`]
892/// the effective scope of **every** unit is `Local`, no matter what the
893/// policy says: a sandboxed flow's reads still chain-read-through to
894/// `World`'s live value on a miss (so it observes current shared state),
895/// but its writes — including to units the policy homes to `World` — land
896/// only in its own `FlowLocal` overrides. `World` is therefore a read-only
897/// base from a sandboxed flow's perspective: nothing it does can mutate the
898/// shared world.
899///
900/// Because the all-`World` policy (the only policy the oracle corpus
901/// exercises) takes the `World` branch on every op *and* no existing
902/// construction path ever sets `Mode::Sandbox`, this is byte-identical to
903/// the F1.3 all-`World` passthrough for every existing single-flow
904/// construction path.
905pub struct ContextView<'a> {
906    world: &'a mut World,
907    local: &'a mut FlowLocal,
908}
909
910impl<'a> ContextView<'a> {
911    /// Build a routing view over a `World` and `FlowLocal` pair for the
912    /// duration of one step.
913    pub fn new(world: &'a mut World, local: &'a mut FlowLocal) -> Self {
914        Self { world, local }
915    }
916
917    /// The scope a unit actually routes by: `policy_scope` in
918    /// [`Mode::Normal`], or unconditionally [`Scope::Local`] in
919    /// [`Mode::Sandbox`] — see the type docs above.
920    #[inline]
921    fn effective_scope(&self, policy_scope: Scope) -> Scope {
922        match self.local.mode {
923            Mode::Normal => policy_scope,
924            Mode::Sandbox => Scope::Local,
925        }
926    }
927}
928
929impl ContextAccess for World {
930    #[inline]
931    fn global(&self, idx: u32) -> &Value {
932        &self.globals[idx as usize]
933    }
934
935    #[inline]
936    fn set_global(&mut self, idx: u32, value: Value) {
937        self.globals[idx as usize] = value;
938    }
939
940    #[inline]
941    fn visit_count(&self, id: DefinitionId) -> u32 {
942        self.visit_counts.get(&id).copied().unwrap_or(0)
943    }
944
945    #[inline]
946    fn increment_visit(&mut self, id: DefinitionId) {
947        *self.visit_counts.entry(id).or_insert(0) += 1;
948    }
949
950    #[inline]
951    fn set_visit_count(&mut self, id: DefinitionId, count: u32) {
952        self.visit_counts.insert(id, count);
953    }
954
955    #[inline]
956    fn turn_count(&self, id: DefinitionId) -> Option<u32> {
957        self.turn_counts.get(&id).copied()
958    }
959
960    #[inline]
961    fn set_turn_count(&mut self, id: DefinitionId, turn: u32) {
962        self.turn_counts.insert(id, turn);
963    }
964
965    #[inline]
966    fn turn_index(&self) -> u32 {
967        self.turn_index
968    }
969
970    #[inline]
971    fn increment_turn_index(&mut self) {
972        self.turn_index += 1;
973    }
974
975    #[inline]
976    fn set_turn_index(&mut self, index: u32) {
977        self.turn_index = index;
978    }
979
980    #[inline]
981    fn rng_seed(&self) -> i32 {
982        self.rng_seed
983    }
984
985    #[inline]
986    fn set_rng_seed(&mut self, seed: i32) {
987        self.rng_seed = seed;
988    }
989
990    #[inline]
991    fn previous_random(&self) -> i32 {
992        self.previous_random
993    }
994
995    #[inline]
996    fn set_previous_random(&mut self, val: i32) {
997        self.previous_random = val;
998    }
999
1000    #[inline]
1001    fn next_random<R: StoryRng>(&self, seed: i32) -> i32 {
1002        let mut rng = R::from_seed(seed);
1003        rng.next_int()
1004    }
1005
1006    fn random_sequence<R: StoryRng>(&self, seed: i32, count: usize) -> Vec<i32> {
1007        let mut rng = R::from_seed(seed);
1008        (0..count).map(|_| rng.next_int()).collect()
1009    }
1010}
1011
1012impl ContextAccess for ContextView<'_> {
1013    #[inline]
1014    fn global(&self, idx: u32) -> &Value {
1015        match self.effective_scope(self.world.policy.scope_of_global(idx)) {
1016            Scope::Local => self
1017                .local
1018                .chain_get_global(idx)
1019                .unwrap_or_else(|| self.world.global(idx)),
1020            Scope::World => self.world.global(idx),
1021        }
1022    }
1023
1024    #[inline]
1025    fn set_global(&mut self, idx: u32, value: Value) {
1026        match self.effective_scope(self.world.policy.scope_of_global(idx)) {
1027            Scope::Local => {
1028                self.local.globals.insert(idx, value);
1029            }
1030            Scope::World => self.world.set_global(idx, value),
1031        }
1032    }
1033
1034    #[inline]
1035    fn visit_count(&self, id: DefinitionId) -> u32 {
1036        match self.effective_scope(self.world.policy.scope_of_knot(id)) {
1037            Scope::Local => self
1038                .local
1039                .chain_get_visit_count(id)
1040                .unwrap_or_else(|| self.world.visit_count(id)),
1041            Scope::World => self.world.visit_count(id),
1042        }
1043    }
1044
1045    #[inline]
1046    fn increment_visit(&mut self, id: DefinitionId) {
1047        match self.effective_scope(self.world.policy.scope_of_knot(id)) {
1048            Scope::Local => {
1049                let base = self.visit_count(id);
1050                self.local.visit_counts.insert(id, base + 1);
1051            }
1052            Scope::World => self.world.increment_visit(id),
1053        }
1054    }
1055
1056    #[inline]
1057    fn set_visit_count(&mut self, id: DefinitionId, count: u32) {
1058        match self.effective_scope(self.world.policy.scope_of_knot(id)) {
1059            Scope::Local => {
1060                self.local.visit_counts.insert(id, count);
1061            }
1062            Scope::World => self.world.set_visit_count(id, count),
1063        }
1064    }
1065
1066    #[inline]
1067    fn turn_count(&self, id: DefinitionId) -> Option<u32> {
1068        match self.effective_scope(self.world.policy.scope_of_knot(id)) {
1069            Scope::Local => self
1070                .local
1071                .chain_get_turn_count(id)
1072                .or_else(|| self.world.turn_count(id)),
1073            Scope::World => self.world.turn_count(id),
1074        }
1075    }
1076
1077    #[inline]
1078    fn set_turn_count(&mut self, id: DefinitionId, turn: u32) {
1079        match self.effective_scope(self.world.policy.scope_of_knot(id)) {
1080            Scope::Local => {
1081                self.local.turn_counts.insert(id, turn);
1082            }
1083            Scope::World => self.world.set_turn_count(id, turn),
1084        }
1085    }
1086
1087    #[inline]
1088    fn turn_index(&self) -> u32 {
1089        match self.effective_scope(self.world.policy.turn_index_scope()) {
1090            Scope::Local => self
1091                .local
1092                .chain_get_turn_index()
1093                .unwrap_or_else(|| self.world.turn_index()),
1094            Scope::World => self.world.turn_index(),
1095        }
1096    }
1097
1098    #[inline]
1099    fn increment_turn_index(&mut self) {
1100        match self.effective_scope(self.world.policy.turn_index_scope()) {
1101            Scope::Local => {
1102                let base = self.turn_index();
1103                self.local.turn_index = Some(base + 1);
1104            }
1105            Scope::World => self.world.increment_turn_index(),
1106        }
1107    }
1108
1109    #[inline]
1110    fn set_turn_index(&mut self, index: u32) {
1111        match self.effective_scope(self.world.policy.turn_index_scope()) {
1112            Scope::Local => {
1113                self.local.turn_index = Some(index);
1114            }
1115            Scope::World => self.world.set_turn_index(index),
1116        }
1117    }
1118
1119    #[inline]
1120    fn rng_seed(&self) -> i32 {
1121        match self.effective_scope(self.world.policy.rng_scope()) {
1122            Scope::Local => self
1123                .local
1124                .chain_get_rng()
1125                .map_or_else(|| self.world.rng_seed(), |rng| rng.seed),
1126            Scope::World => self.world.rng_seed(),
1127        }
1128    }
1129
1130    #[inline]
1131    fn set_rng_seed(&mut self, seed: i32) {
1132        match self.effective_scope(self.world.policy.rng_scope()) {
1133            Scope::Local => {
1134                // CoW from the chain read-through value: seed a fresh local
1135                // override from the base chain's RNG if present, else World.
1136                // (base is always None in F3.1, so this reduces to World.)
1137                let fallback = self.local.chain_get_rng().unwrap_or(LocalRng {
1138                    seed: self.world.rng_seed(),
1139                    previous_random: self.world.previous_random(),
1140                });
1141                let rng = self.local.rng.get_or_insert(fallback);
1142                rng.seed = seed;
1143            }
1144            Scope::World => self.world.set_rng_seed(seed),
1145        }
1146    }
1147
1148    #[inline]
1149    fn previous_random(&self) -> i32 {
1150        match self.effective_scope(self.world.policy.rng_scope()) {
1151            Scope::Local => self
1152                .local
1153                .chain_get_rng()
1154                .map_or_else(|| self.world.previous_random(), |rng| rng.previous_random),
1155            Scope::World => self.world.previous_random(),
1156        }
1157    }
1158
1159    #[inline]
1160    fn set_previous_random(&mut self, val: i32) {
1161        match self.effective_scope(self.world.policy.rng_scope()) {
1162            Scope::Local => {
1163                // CoW from the chain read-through value (see `set_rng_seed`).
1164                let fallback = self.local.chain_get_rng().unwrap_or(LocalRng {
1165                    seed: self.world.rng_seed(),
1166                    previous_random: self.world.previous_random(),
1167                });
1168                let rng = self.local.rng.get_or_insert(fallback);
1169                rng.previous_random = val;
1170            }
1171            Scope::World => self.world.set_previous_random(val),
1172        }
1173    }
1174
1175    #[inline]
1176    fn next_random<R: StoryRng>(&self, seed: i32) -> i32 {
1177        // Pure function of the explicit `seed` argument — not routed
1178        // state, so this delegates to `World`'s implementation unchanged.
1179        // The routed `rng_seed()` above is what call sites read to obtain
1180        // `seed` in the first place.
1181        self.world.next_random::<R>(seed)
1182    }
1183
1184    fn random_sequence<R: StoryRng>(&self, seed: i32, count: usize) -> Vec<i32> {
1185        self.world.random_sequence::<R>(seed, count)
1186    }
1187}
1188
1189#[cfg(test)]
1190mod policy_tests {
1191    use super::*;
1192    use crate::link;
1193
1194    /// Compile a small ink story with the brink compiler and link it, for
1195    /// resolving policies against a real `Program` symbol table.
1196    fn compile(src: &str) -> Program {
1197        let out = brink_compiler::compile("t.ink", |p| {
1198            if p == "t.ink" {
1199                Ok(src.to_string())
1200            } else {
1201                Err(std::io::Error::new(
1202                    std::io::ErrorKind::NotFound,
1203                    "no such include",
1204                ))
1205            }
1206        })
1207        .expect("compile");
1208        let (program, _line_tables) = link(&out.data).expect("link");
1209        program
1210    }
1211
1212    fn sample_program() -> Program {
1213        compile(
1214            "VAR gold = 0\n\
1215             VAR mood = 0\n\
1216             -> shrine\n\
1217             === shrine ===\n\
1218             At the shrine.\n\
1219             -> END\n\
1220             === cellar ===\n\
1221             In the cellar.\n\
1222             -> END\n",
1223        )
1224    }
1225
1226    /// The default `WorldPolicy` (all-`World`) must resolve via the fast
1227    /// path — no name lookups — and every scope must read back as `World`.
1228    #[test]
1229    fn all_world_default_resolves_via_fast_path() {
1230        let program = sample_program();
1231        let policy = WorldPolicy::default();
1232
1233        let resolved = ResolvedPolicy::resolve(&program, &policy).expect("resolves");
1234
1235        // Fast path: no per-slot table populated.
1236        assert!(resolved.global_scopes.is_empty());
1237        assert!(resolved.knot_scopes.is_empty());
1238
1239        let gold_slot = program.global_index("gold").expect("gold declared");
1240        let mood_slot = program.global_index("mood").expect("mood declared");
1241        let shrine_id = program.find_path_target("shrine").expect("shrine exists");
1242        let cellar_id = program.find_path_target("cellar").expect("cellar exists");
1243
1244        assert_eq!(resolved.scope_of_global(gold_slot), Scope::World);
1245        assert_eq!(resolved.scope_of_global(mood_slot), Scope::World);
1246        assert_eq!(resolved.scope_of_knot(shrine_id), Scope::World);
1247        assert_eq!(resolved.scope_of_knot(cellar_id), Scope::World);
1248        assert_eq!(resolved.turn_index_scope(), Scope::World);
1249        assert_eq!(resolved.rng_scope(), Scope::World);
1250    }
1251
1252    /// A policy with `default: Local` plus explicit variable and knot
1253    /// overrides resolves each override to its named scope and leaves
1254    /// everything else at the default.
1255    #[test]
1256    fn resolves_valid_variable_and_knot_overrides() {
1257        let program = sample_program();
1258        let mut overrides = BTreeMap::new();
1259        overrides.insert("gold".to_owned(), Scope::World);
1260        overrides.insert("shrine".to_owned(), Scope::World);
1261        let policy = WorldPolicy {
1262            default: Scope::Local,
1263            overrides,
1264            turn_index: Scope::Local,
1265            rng: Scope::Local,
1266        };
1267
1268        let resolved = ResolvedPolicy::resolve(&program, &policy).expect("resolves");
1269
1270        let gold_slot = program.global_index("gold").expect("gold declared");
1271        let mood_slot = program.global_index("mood").expect("mood declared");
1272        let shrine_id = program.find_path_target("shrine").expect("shrine exists");
1273        let cellar_id = program.find_path_target("cellar").expect("cellar exists");
1274
1275        // Named overrides win.
1276        assert_eq!(resolved.scope_of_global(gold_slot), Scope::World);
1277        assert_eq!(resolved.scope_of_knot(shrine_id), Scope::World);
1278
1279        // Unlisted variable/knot fall back to `default`.
1280        assert_eq!(resolved.scope_of_global(mood_slot), Scope::Local);
1281        assert_eq!(resolved.scope_of_knot(cellar_id), Scope::Local);
1282
1283        // Scalars resolve independently of `overrides`.
1284        assert_eq!(resolved.turn_index_scope(), Scope::Local);
1285        assert_eq!(resolved.rng_scope(), Scope::Local);
1286    }
1287
1288    /// A name in `overrides` that matches neither a declared variable nor a
1289    /// resolvable knot/stitch path is a `PolicyError`, not a silent
1290    /// fallback to `default`.
1291    #[test]
1292    fn unknown_override_name_is_an_error() {
1293        let program = sample_program();
1294        let mut overrides = BTreeMap::new();
1295        overrides.insert("not_a_real_name".to_owned(), Scope::World);
1296        let policy = WorldPolicy {
1297            default: Scope::World,
1298            overrides,
1299            turn_index: Scope::World,
1300            rng: Scope::World,
1301        };
1302
1303        let err = ResolvedPolicy::resolve(&program, &policy).expect_err("must fail");
1304        assert_eq!(err, PolicyError::UnknownName("not_a_real_name".to_owned()));
1305    }
1306
1307    /// `World::new` resolves the policy and constructs a `World` whose
1308    /// globals match the program's declared defaults.
1309    #[test]
1310    fn world_new_resolves_policy_and_initializes_globals() {
1311        let program = sample_program();
1312        let world = World::new(&program, &WorldPolicy::default()).expect("world builds");
1313        assert_eq!(world.globals, program.global_defaults());
1314    }
1315
1316    /// `World::new` propagates the resolver's error for an unknown override
1317    /// name rather than panicking or silently ignoring it.
1318    #[test]
1319    fn world_new_propagates_unknown_name_error() {
1320        let program = sample_program();
1321        let mut overrides = BTreeMap::new();
1322        overrides.insert("nonexistent".to_owned(), Scope::Local);
1323        let policy = WorldPolicy {
1324            overrides,
1325            ..WorldPolicy::default()
1326        };
1327
1328        let err = World::new(&program, &policy).expect_err("must fail");
1329        assert_eq!(err, PolicyError::UnknownName("nonexistent".to_owned()));
1330    }
1331}
1332
1333#[cfg(test)]
1334mod routing_tests {
1335    use super::*;
1336    use crate::link;
1337
1338    /// Compile a small ink story with the brink compiler and link it, for
1339    /// resolving policies against a real `Program` symbol table.
1340    fn compile(src: &str) -> Program {
1341        let out = brink_compiler::compile("t.ink", |p| {
1342            if p == "t.ink" {
1343                Ok(src.to_string())
1344            } else {
1345                Err(std::io::Error::new(
1346                    std::io::ErrorKind::NotFound,
1347                    "no such include",
1348                ))
1349            }
1350        })
1351        .expect("compile");
1352        let (program, _line_tables) = link(&out.data).expect("link");
1353        program
1354    }
1355
1356    fn sample_program() -> Program {
1357        compile(
1358            "VAR gold = 0\n\
1359             VAR mood = 0\n\
1360             -> shrine\n\
1361             === shrine ===\n\
1362             At the shrine.\n\
1363             -> END\n\
1364             === cellar ===\n\
1365             In the cellar.\n\
1366             -> END\n",
1367        )
1368    }
1369
1370    /// `mood` is Local, `gold` stays World (the default). `shrine`/`cellar`
1371    /// are left at the World default too, so visit counts there stay
1372    /// shared — exercised separately below.
1373    fn mixed_policy() -> WorldPolicy {
1374        let mut overrides = BTreeMap::new();
1375        overrides.insert("mood".to_owned(), Scope::Local);
1376        WorldPolicy {
1377            default: Scope::World,
1378            overrides,
1379            turn_index: Scope::World,
1380            rng: Scope::World,
1381        }
1382    }
1383
1384    /// Writing a `Local`-scoped global in one flow must not affect another
1385    /// flow's `ContextView` over the same `World`, nor the `World` itself.
1386    /// Writing a `World`-scoped global in one flow must be immediately
1387    /// visible through another flow's `ContextView`.
1388    #[test]
1389    fn local_write_isolated_world_write_shared() {
1390        let program = sample_program();
1391        let policy = mixed_policy();
1392        let mut world = World::new(&program, &policy).expect("world builds");
1393
1394        let gold_slot = program.global_index("gold").expect("gold declared");
1395        let mood_slot = program.global_index("mood").expect("mood declared");
1396
1397        let mut local_a = FlowLocal::new();
1398        let mut local_b = FlowLocal::new();
1399
1400        // Flow A writes its Local `mood`.
1401        {
1402            let mut view_a = ContextView::new(&mut world, &mut local_a);
1403            view_a.set_global(mood_slot, Value::Int(42));
1404            assert_eq!(view_a.global(mood_slot), &Value::Int(42));
1405        }
1406
1407        // Flow B's view over the same World must not see A's local write —
1408        // it read-throughs to World's untouched default.
1409        {
1410            let view_b = ContextView::new(&mut world, &mut local_b);
1411            assert_eq!(view_b.global(mood_slot), &Value::Int(0));
1412            // World itself is untouched too.
1413            assert_eq!(world.global(mood_slot), &Value::Int(0));
1414        }
1415
1416        // Flow A writes its World-scoped `gold` — this must be immediately
1417        // visible via Flow B's view (and via World directly).
1418        {
1419            let mut view_a = ContextView::new(&mut world, &mut local_a);
1420            view_a.set_global(gold_slot, Value::Int(7));
1421        }
1422        {
1423            let view_b = ContextView::new(&mut world, &mut local_b);
1424            assert_eq!(view_b.global(gold_slot), &Value::Int(7));
1425        }
1426        assert_eq!(world.global(gold_slot), &Value::Int(7));
1427    }
1428
1429    /// Local visit counts increment independently per flow, while a
1430    /// World-scoped knot's visits are shared across flows.
1431    #[test]
1432    fn local_visits_independent_world_visits_shared() {
1433        let program = sample_program();
1434
1435        // shrine: Local: cellar stays at the World default.
1436        let mut overrides = BTreeMap::new();
1437        overrides.insert("shrine".to_owned(), Scope::Local);
1438        let policy = WorldPolicy {
1439            default: Scope::World,
1440            overrides,
1441            turn_index: Scope::World,
1442            rng: Scope::World,
1443        };
1444        let mut world = World::new(&program, &policy).expect("world builds");
1445
1446        let shrine_id = program.find_path_target("shrine").expect("shrine exists");
1447        let cellar_id = program.find_path_target("cellar").expect("cellar exists");
1448
1449        let mut local_a = FlowLocal::new();
1450        let mut local_b = FlowLocal::new();
1451
1452        // Flow A visits `shrine` (Local) twice.
1453        {
1454            let mut view_a = ContextView::new(&mut world, &mut local_a);
1455            view_a.increment_visit(shrine_id);
1456            view_a.increment_visit(shrine_id);
1457            assert_eq!(view_a.visit_count(shrine_id), 2);
1458        }
1459        // Flow B's local shrine count is independent — still 0.
1460        {
1461            let view_b = ContextView::new(&mut world, &mut local_b);
1462            assert_eq!(view_b.visit_count(shrine_id), 0);
1463        }
1464        // World's own bookkeeping for a Local-scoped knot is never touched.
1465        assert_eq!(world.visit_count(shrine_id), 0);
1466
1467        // `cellar` (World-scoped): Flow A's increment is visible to Flow B.
1468        {
1469            let mut view_a = ContextView::new(&mut world, &mut local_a);
1470            view_a.increment_visit(cellar_id);
1471        }
1472        {
1473            let view_b = ContextView::new(&mut world, &mut local_b);
1474            assert_eq!(view_b.visit_count(cellar_id), 1);
1475        }
1476        assert_eq!(world.visit_count(cellar_id), 1);
1477    }
1478
1479    /// A `Local`-scoped global reads through to World's current value until
1480    /// the flow performs its first local write.
1481    #[test]
1482    fn local_read_through_returns_world_default_before_first_write() {
1483        let program = sample_program();
1484        let policy = mixed_policy();
1485        let mut world = World::new(&program, &policy).expect("world builds");
1486        let mood_slot = program.global_index("mood").expect("mood declared");
1487
1488        // World's `mood` starts at the program default (0). A later World
1489        // write (e.g. host bootstrapping) should read through too, since A
1490        // hasn't written its own local override yet.
1491        world.set_global(mood_slot, Value::Int(99));
1492
1493        let mut local_a = FlowLocal::new();
1494        let view_a = ContextView::new(&mut world, &mut local_a);
1495        assert_eq!(view_a.global(mood_slot), &Value::Int(99));
1496    }
1497
1498    /// Local visit-count increment is copy-on-write from the read-through
1499    /// value: if World already has a nonzero count when Local is scoped
1500    /// in, the flow's first local increment starts from that base, not 0.
1501    #[test]
1502    fn local_increment_is_cow_from_read_through_base() {
1503        let program = sample_program();
1504        let mut overrides = BTreeMap::new();
1505        overrides.insert("shrine".to_owned(), Scope::Local);
1506        let policy = WorldPolicy {
1507            default: Scope::World,
1508            overrides,
1509            turn_index: Scope::World,
1510            rng: Scope::World,
1511        };
1512        let mut world = World::new(&program, &policy).expect("world builds");
1513        let shrine_id = program.find_path_target("shrine").expect("shrine exists");
1514
1515        // Seed World's bookkeeping directly (simulating pre-existing shared
1516        // state before this knot was scoped Local for this flow).
1517        world.increment_visit(shrine_id);
1518        world.increment_visit(shrine_id);
1519        assert_eq!(world.visit_count(shrine_id), 2);
1520
1521        let mut local_a = FlowLocal::new();
1522        let mut view_a = ContextView::new(&mut world, &mut local_a);
1523        // First local increment must start from World's base (2), not 0.
1524        view_a.increment_visit(shrine_id);
1525        assert_eq!(view_a.visit_count(shrine_id), 3);
1526
1527        // World's own count is untouched by the Local increment.
1528        assert_eq!(world.visit_count(shrine_id), 2);
1529    }
1530
1531    /// F3.1 chain read-through: a `FlowLocal` with a frozen `base` reads a
1532    /// value from the base when its own top layer has no override, and its
1533    /// own top-layer override shadows the base. Values that appear in
1534    /// neither layer still fall through to `World`.
1535    ///
1536    /// Built by hand (no fork exists yet — that's F3.2): freeze a parent
1537    /// `FlowLocal` that has some overrides, then attach that snapshot as a
1538    /// child's `base`.
1539    #[test]
1540    fn chain_read_through_reads_base_and_top_shadows() {
1541        let program = sample_program();
1542
1543        // Home both globals, both knots, turn index, and RNG to Local so the
1544        // chain (not World) is what's exercised on every read.
1545        let mut overrides = BTreeMap::new();
1546        overrides.insert("gold".to_owned(), Scope::Local);
1547        overrides.insert("mood".to_owned(), Scope::Local);
1548        overrides.insert("shrine".to_owned(), Scope::Local);
1549        overrides.insert("cellar".to_owned(), Scope::Local);
1550        let policy = WorldPolicy {
1551            default: Scope::World,
1552            overrides,
1553            turn_index: Scope::Local,
1554            rng: Scope::Local,
1555        };
1556        let mut world = World::new(&program, &policy).expect("world builds");
1557
1558        let gold_slot = program.global_index("gold").expect("gold declared");
1559        let mood_slot = program.global_index("mood").expect("mood declared");
1560        let shrine_id = program.find_path_target("shrine").expect("shrine exists");
1561        let cellar_id = program.find_path_target("cellar").expect("cellar exists");
1562
1563        // World defaults are distinct from anything we put in the chain, so a
1564        // read hitting World rather than the chain would be visible.
1565        world.set_global(gold_slot, Value::Int(1));
1566        world.set_global(mood_slot, Value::Int(1));
1567
1568        // Build a parent FlowLocal with overrides, then freeze it.
1569        let mut parent = FlowLocal::new();
1570        {
1571            let mut pv = ContextView::new(&mut world, &mut parent);
1572            pv.set_global(gold_slot, Value::Int(100));
1573            pv.set_global(mood_slot, Value::Int(200));
1574            pv.increment_visit(shrine_id); // parent shrine visit = 1
1575            pv.set_turn_count(cellar_id, 5);
1576            pv.increment_turn_index(); // parent turn index = 1
1577            pv.set_rng_seed(777);
1578        }
1579        let base = parent.freeze();
1580
1581        // Child inherits the frozen parent as its base, with its own empty
1582        // top layer.
1583        let mut child = FlowLocal {
1584            base: Some(base),
1585            ..FlowLocal::new()
1586        };
1587
1588        // Reads with an empty top layer see the base's values (not World's).
1589        {
1590            let view = ContextView::new(&mut world, &mut child);
1591            assert_eq!(view.global(gold_slot), &Value::Int(100));
1592            assert_eq!(view.global(mood_slot), &Value::Int(200));
1593            assert_eq!(view.visit_count(shrine_id), 1);
1594            assert_eq!(view.turn_count(cellar_id), Some(5));
1595            assert_eq!(view.turn_index(), 1);
1596            assert_eq!(view.rng_seed(), 777);
1597        }
1598
1599        // A top-layer override shadows the base for that one unit; other
1600        // units keep reading through to the base.
1601        {
1602            let mut view = ContextView::new(&mut world, &mut child);
1603            view.set_global(gold_slot, Value::Int(999));
1604            assert_eq!(view.global(gold_slot), &Value::Int(999)); // shadowed
1605            assert_eq!(view.global(mood_slot), &Value::Int(200)); // still base
1606        }
1607
1608        // A knot with no override anywhere in the chain falls through to
1609        // World (whose count for the Local-scoped `cellar` is untouched: 0).
1610        {
1611            let view = ContextView::new(&mut world, &mut child);
1612            assert_eq!(view.visit_count(cellar_id), 0);
1613        }
1614    }
1615
1616    /// F3.2 fork isolation (`Mode::Normal` child): forking a parent
1617    /// `FlowLocal` gives the child a frozen view of the parent's overrides
1618    /// at fork time. A write the child makes to a `Local`-scoped unit lands
1619    /// only in the child's own top layer — the parent's `FlowLocal` and
1620    /// `World` are both unaffected.
1621    #[test]
1622    fn fork_isolation_normal_child_write_does_not_leak_to_parent_or_world() {
1623        let program = sample_program();
1624        let policy = mixed_policy(); // mood: Local, gold: World (default)
1625        let mut world = World::new(&program, &policy).expect("world builds");
1626        let mood_slot = program.global_index("mood").expect("mood declared");
1627
1628        let mut parent = FlowLocal::new();
1629        {
1630            let mut view = ContextView::new(&mut world, &mut parent);
1631            view.set_global(mood_slot, Value::Int(42));
1632        }
1633
1634        // Fork a Normal child from the parent.
1635        let mut child = parent.fork(Mode::Normal);
1636
1637        // The child reads the parent's frozen state via `base` — it never
1638        // wrote `mood` itself.
1639        {
1640            let view = ContextView::new(&mut world, &mut child);
1641            assert_eq!(view.global(mood_slot), &Value::Int(42));
1642        }
1643
1644        // The child writes its own override for `mood`.
1645        {
1646            let mut view = ContextView::new(&mut world, &mut child);
1647            view.set_global(mood_slot, Value::Int(100));
1648            assert_eq!(view.global(mood_slot), &Value::Int(100));
1649        }
1650
1651        // The parent's own `FlowLocal` still reads its original write — the
1652        // child's write never reached it.
1653        {
1654            let view = ContextView::new(&mut world, &mut parent);
1655            assert_eq!(view.global(mood_slot), &Value::Int(42));
1656        }
1657
1658        // `World` was never touched — `mood` is Local-scoped, so it was
1659        // never written there in the first place.
1660        assert_eq!(world.global(mood_slot), &Value::Int(0));
1661    }
1662
1663    /// F3.2 frozen snapshot: a fork's `base` is a point-in-time snapshot.
1664    /// Mutating the parent *after* the fork must not be visible through the
1665    /// child, which still reads the parent's state as of the fork.
1666    #[test]
1667    fn fork_base_is_a_frozen_snapshot_later_parent_writes_invisible_to_child() {
1668        let program = sample_program();
1669        let policy = mixed_policy(); // mood: Local
1670        let mut world = World::new(&program, &policy).expect("world builds");
1671        let mood_slot = program.global_index("mood").expect("mood declared");
1672
1673        let mut parent = FlowLocal::new();
1674        {
1675            let mut view = ContextView::new(&mut world, &mut parent);
1676            view.set_global(mood_slot, Value::Int(1));
1677        }
1678
1679        let mut child = parent.fork(Mode::Normal);
1680
1681        // Mutate the parent *after* the fork.
1682        {
1683            let mut view = ContextView::new(&mut world, &mut parent);
1684            view.set_global(mood_slot, Value::Int(2));
1685        }
1686
1687        // The child's frozen base still reflects the pre-fork value.
1688        let view = ContextView::new(&mut world, &mut child);
1689        assert_eq!(view.global(mood_slot), &Value::Int(1));
1690    }
1691
1692    /// F3.2 sandbox side-effect-proof: in `Mode::Sandbox`, a `World`-scoped
1693    /// unit is still readable through the live `World` value, but any write
1694    /// — even to a unit the policy homes to `World` — lands only in the
1695    /// sandboxed flow's own overrides. `World` itself is never mutated, and
1696    /// dropping the sandboxed `FlowLocal` leaves no trace.
1697    #[test]
1698    fn sandbox_mode_writes_never_reach_world_reads_see_live_world() {
1699        let program = sample_program();
1700        let policy = mixed_policy(); // gold: World (default), mood: Local
1701        let mut world = World::new(&program, &policy).expect("world builds");
1702        let gold_slot = program.global_index("gold").expect("gold declared");
1703        let shrine_id = program.find_path_target("shrine").expect("shrine exists");
1704
1705        // Simulate pre-existing shared state the sandboxed flow should see.
1706        world.set_global(gold_slot, Value::Int(7));
1707        world.increment_visit(shrine_id);
1708        world.increment_visit(shrine_id);
1709        world.increment_visit(shrine_id);
1710        assert_eq!(world.visit_count(shrine_id), 3);
1711
1712        // Fork a sandboxed child from a "live" flow's (empty) FlowLocal.
1713        let live = FlowLocal::new();
1714        {
1715            let mut sandboxed = live.fork(Mode::Sandbox);
1716
1717            // Reads see World's current, live value even though `gold` and
1718            // `shrine` are World-scoped by policy.
1719            {
1720                let view = ContextView::new(&mut world, &mut sandboxed);
1721                assert_eq!(view.global(gold_slot), &Value::Int(7));
1722                assert_eq!(view.visit_count(shrine_id), 3);
1723            }
1724
1725            // Writing the World-scoped `gold` in the sandbox diverts to the
1726            // sandbox's own overrides — it does not touch `World`.
1727            {
1728                let mut view = ContextView::new(&mut world, &mut sandboxed);
1729                view.set_global(gold_slot, Value::Int(555));
1730                assert_eq!(view.global(gold_slot), &Value::Int(555)); // visible locally
1731            }
1732            assert_eq!(world.global(gold_slot), &Value::Int(7)); // World unchanged
1733
1734            // Incrementing a World-scoped visit count in the sandbox is
1735            // copy-on-write from the live World count, but the increment
1736            // itself stays local — World's count is untouched.
1737            {
1738                let mut view = ContextView::new(&mut world, &mut sandboxed);
1739                view.increment_visit(shrine_id);
1740                assert_eq!(view.visit_count(shrine_id), 4); // sandbox sees 4
1741            }
1742            assert_eq!(world.visit_count(shrine_id), 3); // World still 3
1743
1744            // Dropping `sandboxed` here (end of scope) is discard — nothing
1745            // escaped to World, so there is nothing to unwind.
1746        }
1747
1748        // World is still clean after the sandboxed child is gone.
1749        assert_eq!(world.global(gold_slot), &Value::Int(7));
1750        assert_eq!(world.visit_count(shrine_id), 3);
1751    }
1752}
1753
1754#[cfg(test)]
1755mod save_load_tests {
1756    use super::*;
1757    use crate::link;
1758    use crate::rng::FastRng;
1759    use crate::story::{FallbackHandler, FlowInstance};
1760    use crate::{load_state, save_state};
1761
1762    /// Compile a small ink story with the brink compiler and link it,
1763    /// keeping the line tables `FlowInstance::drive_to_terminal` needs.
1764    fn compile_for_flow(src: &str) -> (Program, Vec<Vec<brink_format::LineEntry>>) {
1765        let out = brink_compiler::compile("t.ink", |p| {
1766            if p == "t.ink" {
1767                Ok(src.to_string())
1768            } else {
1769                Err(std::io::Error::new(
1770                    std::io::ErrorKind::NotFound,
1771                    "no such include",
1772                ))
1773            }
1774        })
1775        .expect("compile");
1776        link(&out.data).expect("link")
1777    }
1778
1779    /// A scoped save/load roundtrip (F6.1b): `gold` (global) and `shrine`
1780    /// (knot) are policy-scoped `Local`; `silver` (global) stays `World`
1781    /// (the default). Driving the flow populates both layers; saving
1782    /// through the routing view captures effective values regardless of
1783    /// scope. Loading into a **fresh** `(World, FlowLocal)` pair through a
1784    /// fresh view must land each unit back in the layer its policy
1785    /// names — `Local` units in the new `FlowLocal`'s override maps (the new
1786    /// `World`'s own copy stays untouched), `World` units directly in the
1787    /// new `World` (the new `FlowLocal` contributes nothing for them).
1788    #[test]
1789    fn scoped_save_load_lands_each_unit_in_its_policy_layer() {
1790        let (program, tables) = compile_for_flow(
1791            "VAR gold = 0\n\
1792             VAR silver = 0\n\
1793             ~ silver = 7\n\
1794             -> shrine\n\
1795             === shrine ===\n\
1796             ~ gold = 5\n\
1797             At the shrine.\n\
1798             -> DONE\n\
1799             === reader ===\n\
1800             {READ_COUNT(-> shrine)}\n\
1801             -> DONE\n",
1802            // `reader` is never entered — it exists only so the compiler's
1803            // counting-flags pass sees a visit-count read of `shrine` and
1804            // sets `CountingFlags::VISITS` on it (a knot whose visit count
1805            // is never read anywhere in the program has counting disabled
1806            // entirely — an existing compiler optimization).
1807        );
1808
1809        let mut overrides = BTreeMap::new();
1810        overrides.insert("gold".to_owned(), Scope::Local);
1811        overrides.insert("shrine".to_owned(), Scope::Local);
1812        let policy = WorldPolicy {
1813            default: Scope::World,
1814            overrides,
1815            turn_index: Scope::World,
1816            rng: Scope::World,
1817        };
1818
1819        let gold_slot = program.global_index("gold").expect("gold declared");
1820        let silver_slot = program.global_index("silver").expect("silver declared");
1821        let shrine_id = program.find_path_target("shrine").expect("shrine exists");
1822
1823        // Drive the flow against a World built from our policy — the
1824        // `FlowInstance::new_at_root`-returned World is discarded; only the
1825        // callstack/thread state it seeds matters here.
1826        let mut world = World::new(&program, &policy).expect("world builds");
1827        let mut local = FlowLocal::new();
1828        let save = {
1829            let (mut flow, _unused_default_world) = FlowInstance::new_at_root(&program);
1830            let mut view = ContextView::new(&mut world, &mut local);
1831            flow.drive_to_terminal::<FastRng>(&program, &tables, &mut view, &FallbackHandler, None)
1832                .expect("drive succeeds");
1833            save_state(&program, &view)
1834        };
1835
1836        assert_eq!(save.globals.get("gold"), Some(&Value::Int(5)));
1837        assert_eq!(save.globals.get("silver"), Some(&Value::Int(7)));
1838        assert_eq!(
1839            save.visits
1840                .iter()
1841                .find(|e| e.id == shrine_id)
1842                .map(|e| e.count),
1843            Some(1),
1844            "shrine should have a captured visit entry"
1845        );
1846
1847        // Load into a fresh (World, FlowLocal) pair, built from the same
1848        // policy but with none of the driven state.
1849        let mut world2 = World::new(&program, &policy).expect("world builds");
1850        let mut local2 = FlowLocal::new();
1851        let report = {
1852            let mut view2 = ContextView::new(&mut world2, &mut local2);
1853            load_state(&program, &mut view2, &save)
1854        };
1855        assert!(report.unknown_globals.is_empty(), "clean load: {report:?}");
1856
1857        // `gold` is Local-scoped: the load must land it in `local2`'s
1858        // override map, leaving `world2`'s own copy at its untouched
1859        // default. The routing view's effective read still sees 5.
1860        assert_eq!(
1861            world2.global(gold_slot),
1862            &Value::Int(0),
1863            "gold is Local-scoped; World's own copy must stay untouched"
1864        );
1865        {
1866            let view2 = ContextView::new(&mut world2, &mut local2);
1867            assert_eq!(view2.global(gold_slot), &Value::Int(5));
1868        }
1869
1870        // `silver` is World-scoped: the load must land it directly in
1871        // `world2`, readable without any FlowLocal involvement.
1872        assert_eq!(
1873            world2.global(silver_slot),
1874            &Value::Int(7),
1875            "silver is World-scoped; must land directly in World"
1876        );
1877
1878        // `shrine`'s visit count is Local-scoped: same split as `gold`.
1879        assert_eq!(
1880            world2.visit_count(shrine_id),
1881            0,
1882            "shrine is Local-scoped; World's own visit count must stay untouched"
1883        );
1884        {
1885            let view2 = ContextView::new(&mut world2, &mut local2);
1886            assert_eq!(view2.visit_count(shrine_id), 1);
1887        }
1888    }
1889}
1890
1891#[cfg(test)]
1892mod subtree_scope_tests {
1893    use super::*;
1894    use crate::link;
1895    use crate::rng::FastRng;
1896    use crate::story::{FallbackHandler, FlowInstance, Line};
1897
1898    /// Compile a small ink story with the brink compiler and link it,
1899    /// keeping the line tables `FlowInstance::drive_to_terminal` needs.
1900    fn compile_for_flow(src: &str) -> (Program, Vec<Vec<brink_format::LineEntry>>) {
1901        let out = brink_compiler::compile("t.ink", |p| {
1902            if p == "t.ink" {
1903                Ok(src.to_string())
1904            } else {
1905                Err(std::io::Error::new(
1906                    std::io::ErrorKind::NotFound,
1907                    "no such include",
1908                ))
1909            }
1910        })
1911        .expect("compile");
1912        link(&out.data).expect("link")
1913    }
1914
1915    /// A knot `guard_talk` with its own top-level stopping sequence
1916    /// (`{ Halt! | Back again? }`) plus a stitch `guard_talk.inner` with its
1917    /// own stopping sequence, and an unrelated `other_knot` — the minimal
1918    /// shape needed to exercise interior-container containment, the knot→
1919    /// stitch cascade, and most-specific-wins precedence.
1920    fn story_with_stitch_and_sequences() -> (Program, Vec<Vec<brink_format::LineEntry>>) {
1921        compile_for_flow(
1922            "VAR gold = 0\n\
1923             -> guard_talk\n\
1924             === guard_talk ===\n\
1925             { stopping: Halt! | Back again? }\n\
1926             -> inner\n\
1927             = inner\n\
1928             { stopping: A | B | C }\n\
1929             -> DONE\n\
1930             === other_knot ===\n\
1931             Other.\n\
1932             -> DONE\n",
1933        )
1934    }
1935
1936    /// Find the `DefinitionId` of the (single) interior container directly
1937    /// owned by `scope_owner` that carries `CountingFlags::VISITS` — i.e.
1938    /// the anonymous sequence container `handle_sequence` (`vm.rs`) keys its
1939    /// counter off. Panics if there isn't exactly one, since every test
1940    /// story here is built with exactly one stopping sequence per scope.
1941    fn find_owned_sequence_id(program: &Program, scope_owner: DefinitionId) -> DefinitionId {
1942        let mut found = None;
1943        for (idx, container) in program.containers.iter().enumerate() {
1944            #[expect(clippy::cast_possible_truncation, reason = "test fixture")]
1945            let idx = idx as u32;
1946            let owner = program.scope_ids[program.scope_table_idx(idx) as usize];
1947            if owner == scope_owner
1948                && container
1949                    .counting_flags
1950                    .contains(brink_format::CountingFlags::VISITS)
1951            {
1952                assert!(
1953                    found.is_none(),
1954                    "expected exactly one VISITS-counted interior container owned by {scope_owner:?}"
1955                );
1956                found = Some(container.id);
1957            }
1958        }
1959        found.expect("expected a VISITS-counted interior container")
1960    }
1961
1962    /// A knot marked `Local` must cover not just its own `DefinitionId` but
1963    /// the interior sequence container nested directly under it — this is
1964    /// the exact bug the F6 AMENDMENT (ruling 3) describes:
1965    /// `handle_sequence` keys a stopping/cycle counter off the sequence's
1966    /// *own* container id, not the enclosing knot's, so without subtree
1967    /// expansion a `Local`-marked knot would silently leave that counter
1968    /// `World`-scoped.
1969    #[test]
1970    fn marked_local_knot_covers_its_interior_sequence_container() {
1971        let (program, _tables) = story_with_stitch_and_sequences();
1972        let guard_talk_id = program
1973            .find_path_target("guard_talk")
1974            .expect("guard_talk exists");
1975        let other_knot_id = program
1976            .find_path_target("other_knot")
1977            .expect("other_knot exists");
1978        let sequence_id = find_owned_sequence_id(&program, guard_talk_id);
1979
1980        let mut overrides = BTreeMap::new();
1981        overrides.insert("guard_talk".to_owned(), Scope::Local);
1982        let policy = WorldPolicy {
1983            default: Scope::World,
1984            overrides,
1985            turn_index: Scope::World,
1986            rng: Scope::World,
1987        };
1988        let resolved = ResolvedPolicy::resolve(&program, &policy).expect("resolves");
1989
1990        assert_eq!(resolved.scope_of_knot(guard_talk_id), Scope::Local);
1991        assert_eq!(
1992            resolved.scope_of_knot(sequence_id),
1993            Scope::Local,
1994            "the interior sequence container must inherit guard_talk's Local scope"
1995        );
1996        // An unrelated knot must stay at the World default — the expansion
1997        // must not leak scope onto unrelated containers.
1998        assert_eq!(resolved.scope_of_knot(other_knot_id), Scope::World);
1999    }
2000
2001    /// Stitch-level override + most-specific-wins precedence: knot
2002    /// `guard_talk` is `Local`, but its stitch `guard_talk.inner` is
2003    /// explicitly `World`. Every container under `inner` (the stitch
2004    /// itself, and its own interior sequence) must resolve `World`; the
2005    /// rest of `guard_talk`'s subtree (the knot's own id and its own
2006    /// interior sequence) must resolve `Local`.
2007    #[test]
2008    fn stitch_override_wins_over_enclosing_knot_for_its_own_subtree() {
2009        let (program, _tables) = story_with_stitch_and_sequences();
2010        let guard_talk_id = program
2011            .find_path_target("guard_talk")
2012            .expect("guard_talk exists");
2013        let inner_id = program
2014            .find_path_target("guard_talk.inner")
2015            .expect("guard_talk.inner exists");
2016        let guard_talk_sequence_id = find_owned_sequence_id(&program, guard_talk_id);
2017        let inner_sequence_id = find_owned_sequence_id(&program, inner_id);
2018
2019        let mut overrides = BTreeMap::new();
2020        overrides.insert("guard_talk".to_owned(), Scope::Local);
2021        overrides.insert("guard_talk.inner".to_owned(), Scope::World);
2022        let policy = WorldPolicy {
2023            default: Scope::World,
2024            overrides,
2025            turn_index: Scope::World,
2026            rng: Scope::World,
2027        };
2028        let resolved = ResolvedPolicy::resolve(&program, &policy).expect("resolves");
2029
2030        assert_eq!(resolved.scope_of_knot(guard_talk_id), Scope::Local);
2031        assert_eq!(resolved.scope_of_knot(guard_talk_sequence_id), Scope::Local);
2032        assert_eq!(
2033            resolved.scope_of_knot(inner_id),
2034            Scope::World,
2035            "the stitch's own explicit override must win over its enclosing knot's"
2036        );
2037        assert_eq!(
2038            resolved.scope_of_knot(inner_sequence_id),
2039            Scope::World,
2040            "the stitch's interior sequence must follow the stitch's own override, \
2041             not the enclosing knot's"
2042        );
2043
2044        // And the reverse precedence: knot World (default), stitch Local —
2045        // confirms precedence isn't just "whichever happens to be Local".
2046        let mut overrides2 = BTreeMap::new();
2047        overrides2.insert("guard_talk".to_owned(), Scope::World);
2048        overrides2.insert("guard_talk.inner".to_owned(), Scope::Local);
2049        let policy2 = WorldPolicy {
2050            default: Scope::World,
2051            overrides: overrides2,
2052            turn_index: Scope::World,
2053            rng: Scope::World,
2054        };
2055        let resolved2 = ResolvedPolicy::resolve(&program, &policy2).expect("resolves");
2056        assert_eq!(resolved2.scope_of_knot(guard_talk_id), Scope::World);
2057        assert_eq!(
2058            resolved2.scope_of_knot(guard_talk_sequence_id),
2059            Scope::World
2060        );
2061        assert_eq!(resolved2.scope_of_knot(inner_id), Scope::Local);
2062        assert_eq!(resolved2.scope_of_knot(inner_sequence_id), Scope::Local);
2063    }
2064
2065    /// The all-`World` default policy must still resolve via
2066    /// `ResolvedPolicy::all_world`'s fast path — no per-slot/per-knot tables
2067    /// populated — even against a program with stitches and sequences that
2068    /// would otherwise drive subtree expansion. This is the oracle-anchored
2069    /// path every existing single-flow construction path takes; it must
2070    /// stay byte-identical.
2071    #[test]
2072    fn all_world_default_still_takes_fast_path() {
2073        let (program, _tables) = story_with_stitch_and_sequences();
2074        let resolved =
2075            ResolvedPolicy::resolve(&program, &WorldPolicy::default()).expect("resolves");
2076
2077        // Fast path: no per-slot/per-knot table populated, matching
2078        // `all_world()` exactly.
2079        assert!(resolved.global_scopes.is_empty());
2080        assert!(resolved.knot_scopes.is_empty());
2081
2082        let guard_talk_id = program
2083            .find_path_target("guard_talk")
2084            .expect("guard_talk exists");
2085        assert_eq!(resolved.scope_of_knot(guard_talk_id), Scope::World);
2086    }
2087
2088    /// An override name that resolves to neither a global nor a knot/
2089    /// stitch path is still a `PolicyError::UnknownName` — subtree
2090    /// expansion must not swallow or change this error path.
2091    #[test]
2092    fn unknown_override_name_still_errors() {
2093        let (program, _tables) = story_with_stitch_and_sequences();
2094        let mut overrides = BTreeMap::new();
2095        overrides.insert("guard_talk.nonexistent_stitch".to_owned(), Scope::Local);
2096        let policy = WorldPolicy {
2097            default: Scope::World,
2098            overrides,
2099            turn_index: Scope::World,
2100            rng: Scope::World,
2101        };
2102        let err = ResolvedPolicy::resolve(&program, &policy).expect_err("must fail");
2103        assert_eq!(
2104            err,
2105            PolicyError::UnknownName("guard_talk.nonexistent_stitch".to_owned())
2106        );
2107    }
2108
2109    /// End-to-end (F6.1c's motivating "per-entity memory" case): two
2110    /// `FlowInstance`s, each with its own `FlowLocal`, drive the *same*
2111    /// `guard_talk` knot (a stopping sequence, `{ Halt! | Back again? }`)
2112    /// over one shared `World` whose policy marks `guard_talk` `Local`.
2113    /// Without subtree expansion, the sequence's own interior container id
2114    /// isn't in `knot_scopes`, falls through to the `World` default, and
2115    /// the two flows' visits collide on one shared counter — the second
2116    /// flow would see "Back again?" on its very first encounter. With the
2117    /// fix, each flow's first encounter is independently the first-visit
2118    /// text.
2119    #[test]
2120    fn two_flows_over_shared_world_each_see_first_visit_text() {
2121        let (program, tables) = compile_for_flow(
2122            "VAR gold = 0\n\
2123             -> guard_talk\n\
2124             === guard_talk ===\n\
2125             { stopping: Halt! | Back again? }\n\
2126             -> DONE\n",
2127        );
2128
2129        let mut overrides = BTreeMap::new();
2130        overrides.insert("guard_talk".to_owned(), Scope::Local);
2131        let policy = WorldPolicy {
2132            default: Scope::World,
2133            overrides,
2134            turn_index: Scope::World,
2135            rng: Scope::World,
2136        };
2137        let mut world = World::new(&program, &policy).expect("world builds");
2138
2139        let drive = |flow: &mut FlowInstance, view: &mut ContextView<'_>| -> String {
2140            let lines = flow
2141                .drive_to_terminal::<FastRng>(&program, &tables, view, &FallbackHandler, None)
2142                .expect("drive succeeds");
2143            assert!(
2144                matches!(lines.last(), Some(Line::Done { .. })),
2145                "expected Done, got {lines:?}"
2146            );
2147            lines.iter().map(Line::text).collect::<String>()
2148        };
2149
2150        // Flow A: first (and only, for this assertion) encounter.
2151        let (mut flow_a, _discarded_world_a) = FlowInstance::new_at_root(&program);
2152        let mut local_a = FlowLocal::new();
2153        let first_visit_a = {
2154            let mut view_a = ContextView::new(&mut world, &mut local_a);
2155            drive(&mut flow_a, &mut view_a)
2156        };
2157
2158        // Flow B: independent FlowLocal, same shared World. Its first
2159        // encounter must read exactly like Flow A's — not "already
2160        // visited" — proving the interior sequence container's visit count
2161        // is Local per-flow, not accidentally shared through World.
2162        let (mut flow_b, _discarded_world_b) = FlowInstance::new_at_root(&program);
2163        let mut local_b = FlowLocal::new();
2164        let first_visit_b = {
2165            let mut view_b = ContextView::new(&mut world, &mut local_b);
2166            drive(&mut flow_b, &mut view_b)
2167        };
2168
2169        assert_eq!(
2170            first_visit_a, first_visit_b,
2171            "both flows' first encounter with guard_talk must produce identical \
2172             (first-visit) text — each flow's visit count is independently local"
2173        );
2174
2175        // Flow A, re-entered a second time (still its own FlowLocal): now
2176        // it must progress to the *next* branch of the stopping sequence,
2177        // proving Local scoping still lets a single flow's own state
2178        // advance normally.
2179        let second_visit_a = {
2180            let mut view_a = ContextView::new(&mut world, &mut local_a);
2181            flow_a
2182                .choose_path_string(&program, &mut view_a, "guard_talk")
2183                .expect("re-enter guard_talk");
2184            drive(&mut flow_a, &mut view_a)
2185        };
2186        assert_ne!(
2187            first_visit_a, second_visit_a,
2188            "flow A's second encounter must progress past the first-visit branch"
2189        );
2190
2191        // Flow B's own local state must still be untouched by flow A's
2192        // second visit — driving B a second time reproduces A's *first*
2193        // progression, not A's second.
2194        let second_visit_b = {
2195            let mut view_b = ContextView::new(&mut world, &mut local_b);
2196            flow_b
2197                .choose_path_string(&program, &mut view_b, "guard_talk")
2198                .expect("re-enter guard_talk");
2199            drive(&mut flow_b, &mut view_b)
2200        };
2201        assert_eq!(
2202            second_visit_a, second_visit_b,
2203            "flow B's second encounter must match flow A's second encounter — \
2204             both progressed independently from the same (shared, untouched) \
2205             World default"
2206        );
2207
2208        // World's own bookkeeping for the Local-scoped knot must never have
2209        // been touched by either flow.
2210        let sequence_id = find_owned_sequence_id(&program, {
2211            program
2212                .find_path_target("guard_talk")
2213                .expect("guard_talk exists")
2214        });
2215        assert_eq!(
2216            world.visit_count(sequence_id),
2217            0,
2218            "World's own copy of the Local-scoped sequence's visit count must stay untouched"
2219        );
2220    }
2221}
2222
2223#[cfg(test)]
2224mod compiled_defaults_tests {
2225    //! Compiled `#@local` scope defaults seeding policy resolution
2226    //! (`docs/directive-annotations-spec.md` §4.6): the base layer of
2227    //! `base ⊕ host-overrides`, with zero host API involvement.
2228
2229    use std::collections::BTreeMap;
2230
2231    use super::*;
2232    use crate::link;
2233
2234    fn compile(src: &str) -> Program {
2235        let out = brink_compiler::compile("t.ink", |p| {
2236            if p == "t.ink" {
2237                Ok(src.to_string())
2238            } else {
2239                Err(std::io::Error::new(
2240                    std::io::ErrorKind::NotFound,
2241                    "no such include",
2242                ))
2243            }
2244        })
2245        .expect("compile");
2246        let (program, _line_tables) = link(&out.data).expect("link");
2247        program
2248    }
2249
2250    /// `mood` and `shrine` (with a stitch and an interior sequence) are
2251    /// marked `#@local` in source; `gold` and `cellar` stay unmarked.
2252    fn annotated_program() -> Program {
2253        compile(
2254            "VAR gold = 0\n\
2255             #@local\n\
2256             VAR mood = 0\n\
2257             -> shrine\n\
2258             === shrine ===\n\
2259             #@local\n\
2260             At the shrine {&once|again}.\n\
2261             = inner\n\
2262             Deeper in.\n\
2263             -> END\n\
2264             === cellar ===\n\
2265             In the cellar.\n\
2266             -> END\n",
2267        )
2268    }
2269
2270    #[test]
2271    fn unannotated_program_keeps_the_fast_path() {
2272        let program = compile("VAR gold = 0\nhello\n");
2273        assert!(!program.has_local_defaults());
2274        let resolved =
2275            ResolvedPolicy::resolve(&program, &WorldPolicy::default()).expect("resolves");
2276        // The all-World fast path allocates nothing.
2277        assert!(resolved.global_scopes.is_empty());
2278        assert!(resolved.knot_scopes.is_empty());
2279    }
2280
2281    #[test]
2282    fn compiled_local_var_seeds_the_base() {
2283        let program = annotated_program();
2284        assert!(program.has_local_defaults());
2285        let resolved =
2286            ResolvedPolicy::resolve(&program, &WorldPolicy::default()).expect("resolves");
2287
2288        let mood = program.global_index("mood").expect("mood declared");
2289        let gold = program.global_index("gold").expect("gold declared");
2290        assert_eq!(resolved.scope_of_global(mood), Scope::Local);
2291        assert_eq!(resolved.scope_of_global(gold), Scope::World);
2292    }
2293
2294    #[test]
2295    fn compiled_local_knot_covers_its_subtree() {
2296        let program = annotated_program();
2297        let resolved =
2298            ResolvedPolicy::resolve(&program, &WorldPolicy::default()).expect("resolves");
2299
2300        let shrine = program.find_path_target("shrine").expect("shrine exists");
2301        let inner = program
2302            .find_path_target("shrine.inner")
2303            .expect("stitch exists");
2304        let cellar = program.find_path_target("cellar").expect("cellar exists");
2305
2306        assert_eq!(resolved.scope_of_knot(shrine), Scope::Local);
2307        assert_eq!(
2308            resolved.scope_of_knot(inner),
2309            Scope::Local,
2310            "a #@local knot covers its stitches"
2311        );
2312        assert_eq!(resolved.scope_of_knot(cellar), Scope::World);
2313
2314        // Interior containers (the inline sequence) are covered too.
2315        let interior = interior_containers_by_scope(&program);
2316        let shrine_interior = interior.get(&shrine).cloned().unwrap_or_default();
2317        assert!(
2318            !shrine_interior.is_empty(),
2319            "the {{&…}} sequence creates interior containers under shrine"
2320        );
2321        for id in shrine_interior {
2322            assert_eq!(
2323                resolved.scope_of_knot(id),
2324                Scope::Local,
2325                "interior container {id:?} inherits the knot's compiled scope"
2326            );
2327        }
2328    }
2329
2330    #[test]
2331    fn host_override_beats_the_compiled_bit() {
2332        let program = annotated_program();
2333        let mut overrides = BTreeMap::new();
2334        overrides.insert("mood".to_owned(), Scope::World);
2335        overrides.insert("shrine".to_owned(), Scope::World);
2336        let policy = WorldPolicy {
2337            default: Scope::World,
2338            overrides,
2339            turn_index: Scope::World,
2340            rng: Scope::World,
2341        };
2342        let resolved = ResolvedPolicy::resolve(&program, &policy).expect("resolves");
2343
2344        let mood = program.global_index("mood").expect("mood declared");
2345        let shrine = program.find_path_target("shrine").expect("shrine exists");
2346        assert_eq!(
2347            resolved.scope_of_global(mood),
2348            Scope::World,
2349            "host override wins over the compiled #@local bit"
2350        );
2351        assert_eq!(resolved.scope_of_knot(shrine), Scope::World);
2352    }
2353
2354    /// End to end with zero host policy: a `World` built with the default
2355    /// (empty) `WorldPolicy` picks up the compiled bits, and two flows
2356    /// sharing it get isolated `mood` but shared `gold`.
2357    #[test]
2358    fn compiled_base_isolates_flows_without_host_policy() {
2359        let program = annotated_program();
2360        let mut world = World::new(&program, &WorldPolicy::default()).expect("world builds");
2361
2362        let mood = program.global_index("mood").expect("mood declared");
2363        let gold = program.global_index("gold").expect("gold declared");
2364
2365        let mut local_a = FlowLocal::new();
2366        let mut local_b = FlowLocal::new();
2367
2368        // Flow A writes both.
2369        {
2370            let mut view_a = ContextView::new(&mut world, &mut local_a);
2371            view_a.set_global(mood, Value::Int(42));
2372            view_a.set_global(gold, Value::Int(7));
2373        }
2374        // Flow B sees the shared `gold` but not A's private `mood`.
2375        {
2376            let view_b = ContextView::new(&mut world, &mut local_b);
2377            assert_eq!(view_b.global(mood), &Value::Int(0));
2378            assert_eq!(view_b.global(gold), &Value::Int(7));
2379        }
2380        assert_eq!(world.global(mood), &Value::Int(0));
2381
2382        // Visit counts: `shrine` is flow-private by compilation.
2383        let shrine = program.find_path_target("shrine").expect("shrine exists");
2384        {
2385            let mut view_a = ContextView::new(&mut world, &mut local_a);
2386            view_a.increment_visit(shrine);
2387            assert_eq!(view_a.visit_count(shrine), 1);
2388        }
2389        {
2390            let view_b = ContextView::new(&mut world, &mut local_b);
2391            assert_eq!(view_b.visit_count(shrine), 0);
2392        }
2393        assert_eq!(world.visit_count(shrine), 0);
2394    }
2395}