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 alloc::boxed::Box;
57use alloc::collections::BTreeMap;
58use alloc::format;
59use alloc::string::String;
60use alloc::sync::Arc;
61use alloc::vec::Vec;
62
63use brink_format::{DefinitionId, Value};
64
65use crate::collections::Map as HashMap;
66use crate::program::Program;
67use crate::rng::StoryRng;
68use crate::state::ContextAccess;
69
70// ── Policy ───────────────────────────────────────────────────────────────
71//
72// The scoped-flow-state model (`docs/scoped-flow-state-spec.md`, "The
73// policy") homes every unit of story-state — globals, visit/turn counts,
74// turn index, RNG — to either the shared `World` or a flow's private
75// `FlowLocal`. `WorldPolicy` is the host-facing, name-based declaration of
76// that split; `ResolvedPolicy` is the fast id/slot-based form the runtime
77// actually consults, built once at `World` creation.
78//
79// F2.1 introduces both shapes and resolution only. `ResolvedPolicy` is
80// stored on `World` but unread — F2.2 wires `ContextView` to consult it.
81
82/// Where a unit of story-state lives: the shared [`World`] or a flow's
83/// private [`FlowLocal`].
84///
85/// `World` is visible to every flow sharing that world immediately on
86/// write — the coordination path. `Local` is private to one flow; it
87/// persists for that flow's lifetime and only folds back into a parent via
88/// an explicit (currently unimplemented, F3) `commit`.
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
90pub enum Scope {
91    /// Shared across every flow over the world. This is the default —
92    /// matches today's single-`Context` behavior byte-for-byte.
93    #[default]
94    World,
95    /// Private to one flow.
96    Local,
97}
98
99/// Host-facing, name-based declaration of the world/local split.
100///
101/// Resolved once (via [`ResolvedPolicy::resolve`]) against a linked
102/// [`Program`]'s symbol table into a fast id/slot-based [`ResolvedPolicy`].
103/// Unlisted variables and knots/stitches fall back to `default`.
104///
105/// The all-`World` default (via [`WorldPolicy::default`]) is the
106/// degenerate, oracle-safety-anchoring policy: every unit homed to
107/// `World`, no overrides — identical to today's single-flow behavior.
108///
109/// **Name precedence:** a name in `overrides` is tried as a global variable
110/// first, then as a knot/stitch path (see [`ResolvedPolicy::resolve`]). If a
111/// name is (unusually) both a declared global VAR and a resolvable knot/
112/// stitch path, the override resolves against the **variable**, never the
113/// knot — the knot path is not consulted once a variable of the same name
114/// is found.
115///
116/// **Knot/stitch overrides are subtree-inclusive** (F6.1c —
117/// `docs/scoped-flow-state-spec.md`'s F6 AMENDMENT, ruling 3): a knot
118/// override covers the knot's own visit/turn count, every stitch nested
119/// directly under it, and every interior container (weave/sequence/choice
120/// container) nested anywhere under the knot or one of its stitches — not
121/// just the knot's own `DefinitionId`. This matters because ink's
122/// sequence/cycle/stopping machinery (`{ Halt! | Back again? }`) keys its
123/// counter off the *sequence's own* interior container id, not the
124/// enclosing knot's id; without subtree inclusion, a knot marked `Local`
125/// would leave those interior counters silently `World`-scoped.
126///
127/// **Most-specific override wins.** If both a knot and one of its stitches
128/// appear in `overrides` (e.g. knot `a` is `Local`, stitch `a.b` is
129/// `World`), every interior container nested under `a.b` resolves `World`;
130/// the rest of `a`'s subtree (the knot's own id, its other stitches, and
131/// any interior container not under `a.b`) resolves `Local`. A stitch's
132/// override always wins over its enclosing knot's for the stitch's own
133/// subtree, regardless of which name appears earlier in `overrides` (see
134/// [`ResolvedPolicy::resolve`] for how this is implemented).
135#[derive(Debug, Clone, Default)]
136pub struct WorldPolicy {
137    /// Scope for any variable or knot/stitch not named in `overrides`.
138    pub default: Scope,
139    /// Per-name exceptions to `default`, for global variables (matched
140    /// against `Program::global_index`'s name grammar) and knot/stitch
141    /// paths (matched against `Program::find_path_target`'s path
142    /// grammar). A name may appear in only one of the two — the resolver
143    /// tries variables first, then knot paths. Knot/stitch overrides are
144    /// subtree-inclusive with most-specific-wins precedence — see the type
145    /// docs above.
146    pub overrides: BTreeMap<String, Scope>,
147    /// Scope of the turn index (a single scalar field).
148    pub turn_index: Scope,
149    /// Scope of the RNG stream (`rng_seed` + `previous_random`, a single
150    /// scalar stream). See the spec's determinism caveat: a `World`-scoped
151    /// RNG interleaves draws from every flow sharing the world by
152    /// execution order.
153    pub rng: Scope,
154}
155
156/// Errors resolving a [`WorldPolicy`] against a [`Program`]'s symbol table.
157///
158/// Resolution happens once, at `World` creation — an unknown name here is
159/// a host configuration error, not a runtime one.
160#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
161pub enum PolicyError {
162    /// A name in `WorldPolicy::overrides` matched neither a declared
163    /// global variable nor a resolvable knot/stitch path.
164    #[error("unknown variable or knot/stitch in world policy overrides: {0}")]
165    UnknownName(String),
166}
167
168/// Fast, id/slot-based resolution of a [`WorldPolicy`] against a specific
169/// [`Program`]. Built once at `World` creation via
170/// [`ResolvedPolicy::resolve`]; consulted on every state access (from F2.2
171/// on) with O(1) lookups — no string matching on the hot path.
172#[derive(Debug, Clone)]
173pub struct ResolvedPolicy {
174    /// Default scope for globals/knots not otherwise listed.
175    default: Scope,
176    /// Per-slot scope for every global variable, dense (length ==
177    /// `Program::global_count()`). Populated with `default` for slots with
178    /// no override, so lookups never need a fallback branch.
179    global_scopes: Vec<Scope>,
180    /// Non-default scope for a knot/stitch (or an interior weave/sequence/
181    /// choice container nested under one), keyed by its defining
182    /// `DefinitionId` (the same id `ContextAccess::visit_count` and friends
183    /// are called with — e.g. `vm.rs`'s `handle_sequence` keys a stopping/
184    /// cycle sequence's counter off the *sequence's own* interior container
185    /// id, not its enclosing knot's).
186    ///
187    /// **Subtree-inclusive (F6.1c):** an override on a knot/stitch name is
188    /// expanded at resolve time (see [`resolve`](Self::resolve)) to cover
189    /// its own id, every stitch nested directly under it (if it's a knot),
190    /// and every interior container nested anywhere under it or one of its
191    /// stitches — not just the literal `DefinitionId` the override name
192    /// resolved to. Only exceptions to `default` are stored — sparse, since
193    /// most programs have far more knots/interior containers than
194    /// overrides.
195    knot_scopes: HashMap<DefinitionId, Scope>,
196    /// Scope of the turn index.
197    turn_index: Scope,
198    /// Scope of the RNG stream.
199    rng: Scope,
200}
201
202impl ResolvedPolicy {
203    /// The all-`World` resolved policy — no name lookups needed. This is
204    /// the fast path for [`WorldPolicy::default()`] and the only policy
205    /// exercised by the oracle-anchored single-flow path.
206    #[must_use]
207    pub fn all_world() -> Self {
208        Self {
209            default: Scope::World,
210            global_scopes: Vec::new(),
211            knot_scopes: HashMap::new(),
212            turn_index: Scope::World,
213            rng: Scope::World,
214        }
215    }
216
217    /// Resolve a host-facing [`WorldPolicy`] against a linked `Program`'s
218    /// symbol table.
219    ///
220    /// Variable names are resolved via [`Program::global_index`]; knot/
221    /// stitch paths via `Program`'s path-to-`DefinitionId` resolution
222    /// (the same table `find_address`/`find_path_target` use). A name is
223    /// tried as a variable first, then as a knot/stitch path; a name
224    /// matching neither is a [`PolicyError::UnknownName`].
225    ///
226    /// **Subtree expansion (F6.1c).** Every knot/stitch override is
227    /// expanded here, once, into every `DefinitionId` in its definition
228    /// subtree — see [`expand_knot_scope`] for the containment mechanism
229    /// (`Program::scope_ids`, the nearest-enclosing-scope table every
230    /// interior container carries, plus `Program::address_by_path`'s
231    /// dotted knot/stitch grammar for the one-level knot→stitch link that
232    /// `scope_ids` alone doesn't carry). `overrides` is a `BTreeMap`, so
233    /// this loop's iteration order is deterministic (sorted by name) — and
234    /// because a knot's name is always a proper prefix of (and therefore
235    /// sorts lexicographically before) any of its stitches' names, a knot
236    /// override's subtree expansion always runs *before* a same-subtree
237    /// stitch override in this same pass, so the stitch override's own
238    /// `insert`s (which land later) win — implementing "most-specific
239    /// override wins" as a natural consequence of processing order, no
240    /// separate precedence pass needed.
241    ///
242    /// **Compiled base layer.** Before host overrides apply, `resolve`
243    /// seeds scopes from the `Program`'s compiled `#@local` defaults
244    /// (`docs/directive-annotations-spec.md`): globals the compiler
245    /// marked flow-private seed `Local`, and every `#@local` knot/stitch
246    /// expands over its subtree exactly like a host override would.
247    /// Host overrides then layer on top — `base ⊕ host-overrides` — so a
248    /// host name always beats the compiled bit for that name's subtree.
249    ///
250    /// The all-`World` default (empty `overrides`, no compiled `#@local`
251    /// bits) resolves without any name lookups (see
252    /// [`all_world`](Self::all_world)) — this is the fast path every
253    /// unannotated single-flow program takes.
254    pub fn resolve(program: &Program, policy: &WorldPolicy) -> Result<Self, PolicyError> {
255        if policy.overrides.is_empty()
256            && policy.default == Scope::World
257            && policy.turn_index == Scope::World
258            && policy.rng == Scope::World
259            && !program.has_local_defaults()
260        {
261            return Ok(Self::all_world());
262        }
263
264        // Seed globals from the compiled base: `#@local` beats the host
265        // default; everything unmarked follows the host default.
266        let mut global_scopes: Vec<Scope> = (0..program.global_count())
267            .map(|slot| {
268                if program.global_is_local(slot) {
269                    Scope::Local
270                } else {
271                    policy.default
272                }
273            })
274            .collect();
275        let mut knot_scopes = HashMap::new();
276        let interior_by_scope = interior_containers_by_scope(program);
277
278        // Seed knots/stitches from the compiled base. The list is sorted
279        // by path at link time, so a `#@local` knot expands before any of
280        // its own `#@local` stitches — same ordering argument as the
281        // override loop below.
282        for (path, id) in program.local_scope_defaults() {
283            expand_knot_scope(
284                program,
285                &interior_by_scope,
286                &mut knot_scopes,
287                path,
288                *id,
289                Scope::Local,
290            );
291        }
292
293        // `overrides` is a `BTreeMap`, so iteration order is deterministic
294        // (sorted by name) — resolution never depends on hash-map order.
295        for (name, &scope) in &policy.overrides {
296            if let Some(slot) = program.global_index(name) {
297                global_scopes[slot as usize] = scope;
298            } else if let Some(id) = program.find_path_target(name) {
299                expand_knot_scope(
300                    program,
301                    &interior_by_scope,
302                    &mut knot_scopes,
303                    name,
304                    id,
305                    scope,
306                );
307            } else {
308                return Err(PolicyError::UnknownName(name.clone()));
309            }
310        }
311
312        Ok(Self {
313            default: policy.default,
314            global_scopes,
315            knot_scopes,
316            turn_index: policy.turn_index,
317            rng: policy.rng,
318        })
319    }
320
321    /// Scope of a global variable by slot index.
322    #[must_use]
323    pub fn scope_of_global(&self, slot: u32) -> Scope {
324        self.global_scopes
325            .get(slot as usize)
326            .copied()
327            .unwrap_or(self.default)
328    }
329
330    /// Scope of a knot/stitch — or of an interior weave/sequence/choice
331    /// container nested under one — by its defining `DefinitionId`. See the
332    /// `knot_scopes` field docs and [`resolve`](Self::resolve) for how a
333    /// knot/stitch override is expanded, at resolve time, to cover every id
334    /// in its definition subtree.
335    #[must_use]
336    pub fn scope_of_knot(&self, id: DefinitionId) -> Scope {
337        self.knot_scopes.get(&id).copied().unwrap_or(self.default)
338    }
339
340    /// Scope of the turn index.
341    #[must_use]
342    pub fn turn_index_scope(&self) -> Scope {
343        self.turn_index
344    }
345
346    /// Scope of the RNG stream.
347    #[must_use]
348    pub fn rng_scope(&self) -> Scope {
349        self.rng
350    }
351}
352
353// ── Subtree-inclusive knot scope (F6.1c) ────────────────────────────────
354//
355// Containment mechanism, verified against a compiled `Program` (see the
356// `subtree_scope_tests` module below and the investigation in the F6.1c
357// build log — not restated here):
358//
359// - `ContainerDef::scope_id` (preserved on `Program` as the parallel
360//   `scope_ids`/`scope_table_idx` tables) gives every container — knot,
361//   stitch, root, or an anonymous interior weave/sequence/choice
362//   container, at any nesting depth — the `DefinitionId` of its *nearest
363//   enclosing* knot/stitch/root scope, correctly propagated through
364//   arbitrarily deep nesting by the codegen's recursive walk. A container
365//   is itself a scope owner (a knot, stitch, or root) exactly when its own
366//   `scope_id` equals its own id — self-scoped, no parent. This gives an
367//   exact, structural (not name-based) map from any interior container to
368//   its owning knot/stitch: `interior_containers_by_scope`, below.
369// - `scope_id` does *not* link a stitch to its enclosing knot (a stitch is
370//   self-scoped, by the same rule above) — ink only nests stitches one
371//   level under a knot, and that one link has to come from
372//   `Program::address_by_path`'s dotted qualified-path grammar (the same
373//   table `find_address`/`find_path_target` already use): a knot named `N`
374//   knows its direct stitches are exactly the `address_by_path` entries
375//   `"N.<segment>"` with no further `.` in `<segment>`, whose target is
376//   itself a scope owner (ruling out an author-labeled gather directly in
377//   the knot, which shares the same two-segment path shape but is *not*
378//   self-scoped).
379//
380// Both of these are compile-time/link-time structural facts already
381// present on `Program` — no id arithmetic or heuristic string matching on
382// unstructured names.
383
384/// Group every non-scope-owning ("interior") container's own id by the
385/// `DefinitionId` of its nearest enclosing knot/stitch/root scope.
386///
387/// Built once per non-fast-path [`ResolvedPolicy::resolve`] call — this is
388/// resolve-time bookkeeping, not a hot-path lookup (the all-`World` fast
389/// path never calls this at all).
390fn interior_containers_by_scope(program: &Program) -> HashMap<DefinitionId, Vec<DefinitionId>> {
391    let mut by_scope: HashMap<DefinitionId, Vec<DefinitionId>> = HashMap::new();
392    for (idx, container) in program.containers.iter().enumerate() {
393        #[expect(
394            clippy::cast_possible_truncation,
395            reason = "container count fits in u32"
396        )]
397        let idx = idx as u32;
398        let owner = program.scope_ids[program.scope_table_idx(idx) as usize];
399        if owner != container.id {
400            by_scope.entry(owner).or_default().push(container.id);
401        }
402    }
403    by_scope
404}
405
406/// Apply `scope` to `id` and every interior container `interior_by_scope`
407/// says is nested directly under it (i.e. `id`'s own subtree, one scope
408/// level — not a recursive walk, since ink knots/stitches never nest
409/// containers whose `scope_id` chain needs more than one enclosing-scope
410/// hop to resolve; see `interior_containers_by_scope`).
411fn apply_scope_to_subtree(
412    interior_by_scope: &HashMap<DefinitionId, Vec<DefinitionId>>,
413    knot_scopes: &mut HashMap<DefinitionId, Scope>,
414    id: DefinitionId,
415    scope: Scope,
416) {
417    knot_scopes.insert(id, scope);
418    if let Some(interior) = interior_by_scope.get(&id) {
419        for &child_id in interior {
420            knot_scopes.insert(child_id, scope);
421        }
422    }
423}
424
425/// Expand a single `WorldPolicy::overrides` knot/stitch entry (`name` →
426/// `id`, already resolved via `Program::find_path_target`) into every
427/// `DefinitionId` in its definition subtree, writing `scope` for each into
428/// `knot_scopes`.
429///
430/// Covers: `id`'s own subtree (its own id plus its direct interior
431/// containers), then — since a stitch override's own call to this function
432/// already covers everything a stitch can own, and ink never nests a
433/// stitch under another stitch — cascades once to `name`'s direct child
434/// stitches (found via `address_by_path`'s dotted grammar, see the module
435/// docs above) and covers each of *their* subtrees too. A child stitch
436/// that itself has a more specific override is still safe to cascade into
437/// here: `resolve`'s `BTreeMap` iteration order guarantees the stitch's own
438/// (later, more specific) entry is processed after `name`'s and overwrites
439/// whatever this cascade wrote — see `resolve`'s docs.
440fn expand_knot_scope(
441    program: &Program,
442    interior_by_scope: &HashMap<DefinitionId, Vec<DefinitionId>>,
443    knot_scopes: &mut HashMap<DefinitionId, Scope>,
444    name: &str,
445    id: DefinitionId,
446    scope: Scope,
447) {
448    apply_scope_to_subtree(interior_by_scope, knot_scopes, id, scope);
449
450    let prefix = format!("{name}.");
451    for (path, target) in &program.address_by_path {
452        let Some(rest) = path.strip_prefix(prefix.as_str()) else {
453            continue;
454        };
455        if rest.is_empty() || rest.contains('.') {
456            continue; // Not a direct one-segment child of `name`.
457        }
458        if target.byte_offset != 0 {
459            continue; // Not a container's own primary address.
460        }
461        // Confirm the target is itself a scope-owning container (a real
462        // stitch), not an author-labeled gather directly in the knot that
463        // happens to share the same two-segment path shape.
464        let owner = program.scope_ids[program.scope_table_idx(target.container_idx) as usize];
465        if owner != target.id {
466            continue;
467        }
468        apply_scope_to_subtree(interior_by_scope, knot_scopes, target.id, scope);
469    }
470}
471
472/// Shared game state that lives above individual flows.
473///
474/// Holds globals, visit/turn tracking, and RNG state. This is the natural
475/// serialization boundary for save/load (deferred).
476///
477/// Multiple [`FlowInstance`](crate::FlowInstance)s can share a single
478/// `World` (matching inklecate's semantics where flow writes are
479/// immediately visible to other flows), or each flow can hold its own
480/// cloned `World` if the consumer wants fork/branch/rollback semantics.
481/// The runtime's step functions take `&mut World` (or any
482/// `&mut impl ContextAccess`) without prescribing where it lives.
483#[derive(Debug, Clone)]
484pub struct World {
485    pub globals: Vec<Value>,
486    pub visit_counts: HashMap<DefinitionId, u32>,
487    pub turn_counts: HashMap<DefinitionId, u32>,
488    pub turn_index: u32,
489    pub rng_seed: i32,
490    pub previous_random: i32,
491    /// The resolved world/local scoping policy for this world.
492    ///
493    /// Boxed so `World` (cloned per-flow-spawn and stored inline in several
494    /// call sites and enums across the crate graph) doesn't balloon in size
495    /// for consumers that never touch policy — `ResolvedPolicy` carries a
496    /// per-slot `Vec` and a `HashMap` that dwarf `World`'s other fields.
497    ///
498    /// **Consulted by [`ContextView`] (F2.2 on)** to route every
499    /// [`ContextAccess`] op between `World` and `FlowLocal`. Every
500    /// construction path that predates policy (`World::from_globals`,
501    /// `FlowInstance::new_at*`, `Story::new`) resolves
502    /// [`WorldPolicy::default()`] (all-`World`), so those paths route every
503    /// op straight to `World` — unchanged from F1.3.
504    policy: Box<ResolvedPolicy>,
505}
506
507impl World {
508    /// Create a fresh `World` for `program`, resolving `policy` against the
509    /// program's symbol table.
510    ///
511    /// Globals are initialized from the program's declared defaults; visit
512    /// counts, turn counts, turn index, and RNG state start zeroed —
513    /// identical to `FlowInstance::new_at`'s inline construction. Fails if
514    /// `policy` names a variable or knot/stitch the program doesn't
515    /// declare ([`PolicyError::UnknownName`]).
516    ///
517    /// [`WorldPolicy::default()`] (all-`World`) always resolves — see
518    /// [`ResolvedPolicy::all_world`] — so passing it here can't produce a
519    /// `PolicyError`.
520    pub fn new(program: &Program, policy: &WorldPolicy) -> Result<Self, PolicyError> {
521        let resolved = ResolvedPolicy::resolve(program, policy)?;
522        Ok(Self::from_globals(program.global_defaults(), resolved))
523    }
524
525    /// Build a `World` from an explicit globals vector and an
526    /// already-resolved policy. Used by [`crate::FlowInstance::new_at`] (whose
527    /// signature predates policy and can't take a `Result`) to construct
528    /// the all-`World` default without re-deriving it from a `Program` each
529    /// time.
530    pub(crate) fn from_globals(globals: Vec<Value>, policy: ResolvedPolicy) -> Self {
531        Self {
532            globals,
533            visit_counts: HashMap::new(),
534            turn_counts: HashMap::new(),
535            turn_index: 0,
536            rng_seed: 0,
537            previous_random: 0,
538            policy: Box::new(policy),
539        }
540    }
541
542    /// Construct a `World` directly from its field values, with the
543    /// all-`World` policy. Only for test fixtures that need to hand-build a
544    /// `World` without a `Program` (e.g. `bevy-brink`'s commit-merge
545    /// tests) — production code should go through [`World::new`].
546    #[cfg(feature = "testing")]
547    #[must_use]
548    pub fn new_for_testing(
549        globals: Vec<Value>,
550        visit_counts: HashMap<DefinitionId, u32>,
551        turn_counts: HashMap<DefinitionId, u32>,
552        turn_index: u32,
553        rng_seed: i32,
554        previous_random: i32,
555    ) -> Self {
556        Self {
557            globals,
558            visit_counts,
559            turn_counts,
560            turn_index,
561            rng_seed,
562            previous_random,
563            policy: Box::new(ResolvedPolicy::all_world()),
564        }
565    }
566}
567
568/// A flow-local override of the shared RNG stream (`rng_seed` +
569/// `previous_random`), the two scalars [`WorldPolicy::rng`] scopes as a
570/// single unit.
571#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
572pub struct LocalRng {
573    pub seed: i32,
574    pub previous_random: i32,
575}
576
577/// An immutable snapshot of a [`FlowLocal`]'s override layer, frozen via
578/// [`FlowLocal::freeze`].
579///
580/// `FrozenLocal` chains: its own `base` is whatever the source `FlowLocal`'s
581/// base was at freeze time, so a chain of forks ([`FlowLocal::fork`]) can
582/// walk arbitrarily far back through frozen ancestors. Cloning a
583/// `FrozenLocal` reference is cheap — callers hold it behind an [`Arc`].
584#[derive(Debug, Clone, Default)]
585pub struct FrozenLocal {
586    /// Overridden values for globals `ResolvedPolicy` homes to `Local`,
587    /// keyed by slot index.
588    globals: BTreeMap<u32, Value>,
589    /// Overridden visit counts for knots/stitches homed to `Local`.
590    visit_counts: BTreeMap<DefinitionId, u32>,
591    /// Overridden turn counts for knots homed to `Local`.
592    turn_counts: BTreeMap<DefinitionId, u32>,
593    /// Overridden turn index, when `turn_index_scope() == Local`.
594    turn_index: Option<u32>,
595    /// Overridden RNG stream, when `rng_scope() == Local`.
596    rng: Option<LocalRng>,
597    /// The next link in the chain, if this snapshot's source `FlowLocal`
598    /// itself had a base at freeze time.
599    base: Option<Arc<FrozenLocal>>,
600}
601
602impl FrozenLocal {
603    /// Chain-lookup a global override: this snapshot's own overrides, else
604    /// recurse into `base`. Returns `None` on a miss all the way down the
605    /// chain — the caller falls through to `World`.
606    fn chain_get_global(&self, idx: u32) -> Option<&Value> {
607        self.globals
608            .get(&idx)
609            .or_else(|| self.base.as_deref().and_then(|b| b.chain_get_global(idx)))
610    }
611
612    /// Chain-lookup a visit-count override.
613    fn chain_get_visit_count(&self, id: DefinitionId) -> Option<u32> {
614        self.visit_counts.get(&id).copied().or_else(|| {
615            self.base
616                .as_deref()
617                .and_then(|b| b.chain_get_visit_count(id))
618        })
619    }
620
621    /// Chain-lookup a turn-count override.
622    fn chain_get_turn_count(&self, id: DefinitionId) -> Option<u32> {
623        self.turn_counts.get(&id).copied().or_else(|| {
624            self.base
625                .as_deref()
626                .and_then(|b| b.chain_get_turn_count(id))
627        })
628    }
629
630    /// Chain-lookup the overridden turn index.
631    fn chain_get_turn_index(&self) -> Option<u32> {
632        self.turn_index.or_else(|| {
633            self.base
634                .as_deref()
635                .and_then(FrozenLocal::chain_get_turn_index)
636        })
637    }
638
639    /// Chain-lookup the overridden RNG stream.
640    fn chain_get_rng(&self) -> Option<LocalRng> {
641        self.rng
642            .or_else(|| self.base.as_deref().and_then(FrozenLocal::chain_get_rng))
643    }
644}
645
646/// Execution mode of a [`FlowLocal`], baked in at construction/fork time and
647/// read by [`ContextView`] to decide how it routes every unit.
648///
649/// `Mode` is orthogonal to [`WorldPolicy`]/[`ResolvedPolicy`]: policy homes a
650/// *unit* (a global, a knot's visit count, …) to `World` or `Local`; `Mode`
651/// decides, for *this flow*, whether that homing is honored at all.
652#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
653pub enum Mode {
654    /// Route every unit by policy, exactly as [`ContextView`]'s F2.2/F3.1
655    /// docs describe: `World`-scoped units go straight to `World`,
656    /// `Local`-scoped units chain-read-through/write to `FlowLocal`. Every
657    /// construction path before F3.2 produces `Mode::Normal` — this is what
658    /// keeps the oracle corpus byte-identical.
659    #[default]
660    Normal,
661    /// Treat **every** unit as `Local`, regardless of policy: the shared
662    /// `World` becomes a read-only base for this flow. Reads still
663    /// chain-read-through to `World`'s current value on a total miss (so a
664    /// sandboxed flow sees live world state), but writes always land in
665    /// this flow's own top-layer overrides — `World` (and any `Normal`
666    /// ancestor) is never mutated. Combined with [`FlowLocal::fork`]'s
667    /// frozen base, this is the side-effect-proof primitive watch/eval
668    /// needs: run a flow against current state, observe its output, then
669    /// discard it (drop) with the shared world untouched.
670    Sandbox,
671}
672
673/// Per-flow override layer over the shared [`World`].
674///
675/// **F3.1: copy-on-write, frozen-base read-through chain.** Each field is a
676/// plain map/option holding this flow's own overrides for units
677/// [`ResolvedPolicy`] homes to [`Scope::Local`] (or, in [`Mode::Sandbox`],
678/// *every* unit — see [`Mode`]), plus an optional `base`: an immutable
679/// [`FrozenLocal`] snapshot (see [`FlowLocal::freeze`]) of another
680/// `FlowLocal`, captured at some earlier point. A read walks **own
681/// overrides → base (recursively) → [miss]**; [`ContextView`] treats a miss
682/// as "not in the local chain" and falls through to `World`, exactly as in
683/// F2.2. Writes always land in the flow's own top-layer overrides — never
684/// in `base`, which is immutable by construction.
685///
686/// A fresh `FlowLocal` (via `Default`/[`FlowLocal::new`]) has empty
687/// overrides, `base: None`, and `mode: Mode::Normal`, so it contributes no
688/// reads and every access falls through to `World` — this is what keeps the
689/// all-`World` policy (and every construction path that doesn't call
690/// [`fork`](Self::fork)) byte-identical to the F2.2 flat-storage behavior.
691/// [`FlowLocal::fork`] (F3.2) is what actually populates a child's `base` by
692/// freezing its parent, and what bakes in a non-`Normal` `mode`.
693///
694/// [`ContextView`] (below) is what actually consults these maps; see its
695/// docs for the read-through/copy-on-write-increment/mode semantics.
696#[derive(Debug, Clone, Default)]
697pub struct FlowLocal {
698    /// Overridden values for globals `ResolvedPolicy` homes to `Local`,
699    /// keyed by slot index.
700    globals: BTreeMap<u32, Value>,
701    /// Overridden visit counts for knots/stitches homed to `Local`.
702    visit_counts: BTreeMap<DefinitionId, u32>,
703    /// Overridden turn counts for knots homed to `Local`.
704    turn_counts: BTreeMap<DefinitionId, u32>,
705    /// Overridden turn index, when `turn_index_scope() == Local`.
706    turn_index: Option<u32>,
707    /// Overridden RNG stream, when `rng_scope() == Local`.
708    rng: Option<LocalRng>,
709    /// Frozen snapshot of an earlier `FlowLocal`'s overrides, read *after*
710    /// this layer's own overrides on a miss. Populated by [`FlowLocal::fork`];
711    /// `None` for every construction path that doesn't fork.
712    base: Option<Arc<FrozenLocal>>,
713    /// This flow's execution mode — see [`Mode`]. Baked in at construction
714    /// (`Mode::Normal`, the `Default`) or at [`FlowLocal::fork`] time.
715    mode: Mode,
716}
717
718impl FlowLocal {
719    /// Construct an empty flow-local layer — overrides nothing and has no
720    /// base, so every access routes through to `World`.
721    #[must_use]
722    pub fn new() -> Self {
723        Self::default()
724    }
725
726    /// Freeze this `FlowLocal`'s current state into an immutable
727    /// [`FrozenLocal`] snapshot, suitable for use as another `FlowLocal`'s
728    /// `base`.
729    ///
730    /// Captures the override maps (cloned — cheap, since only `Local`-scoped
731    /// units are ever present) and cheap-clones the current `base` `Arc` so
732    /// the new snapshot chains to the same ancestry this `FlowLocal` had.
733    ///
734    /// Called by [`FlowLocal::fork`] to snapshot a parent into a child's
735    /// `base`.
736    #[must_use]
737    fn freeze(&self) -> Arc<FrozenLocal> {
738        Arc::new(FrozenLocal {
739            globals: self.globals.clone(),
740            visit_counts: self.visit_counts.clone(),
741            turn_counts: self.turn_counts.clone(),
742            turn_index: self.turn_index,
743            rng: self.rng,
744            base: self.base.clone(),
745        })
746    }
747
748    /// Fork a child `FlowLocal` from this one.
749    ///
750    /// The child's `base` is a frozen, point-in-time snapshot of `self` (via
751    /// [`freeze`](Self::freeze)) — an `O(1)`-ish operation that clones this
752    /// flow's own (small) override maps and `Arc`-bumps the rest of the
753    /// ancestry chain, never a full `World` copy. The child's own override
754    /// layer starts empty, and it runs in `mode` for its lifetime (`Mode` is
755    /// baked in here, not mutable afterward).
756    ///
757    /// Because the base is frozen, later mutations to `self` (the parent)
758    /// are **not** visible to the child — the child sees the parent exactly
759    /// as it was at fork time. Symmetrically, nothing the child does is ever
760    /// visible to `self` or `World`: writes land only in the child's own top
761    /// layer (see [`Mode`] for how `Sandbox` additionally diverts
762    /// `World`-scoped writes there too). That makes **discard** trivial —
763    /// dropping the returned `FlowLocal` is the entire discard operation, no
764    /// unwinding required. Folding a child's writes back into `self` instead
765    /// of discarding them is the deferred `commit` seam — see [`CommitError`]
766    /// and [`commit`].
767    #[must_use]
768    pub fn fork(&self, mode: Mode) -> FlowLocal {
769        FlowLocal {
770            base: Some(self.freeze()),
771            mode,
772            ..FlowLocal::new()
773        }
774    }
775
776    /// Chain-lookup a global override: own overrides → `base` (recursively)
777    /// → `None` on a total miss, which [`ContextView`] treats as "fall
778    /// through to `World`".
779    fn chain_get_global(&self, idx: u32) -> Option<&Value> {
780        self.globals
781            .get(&idx)
782            .or_else(|| self.base.as_deref().and_then(|b| b.chain_get_global(idx)))
783    }
784
785    /// Chain-lookup a visit-count override.
786    fn chain_get_visit_count(&self, id: DefinitionId) -> Option<u32> {
787        self.visit_counts.get(&id).copied().or_else(|| {
788            self.base
789                .as_deref()
790                .and_then(|b| b.chain_get_visit_count(id))
791        })
792    }
793
794    /// Chain-lookup a turn-count override.
795    fn chain_get_turn_count(&self, id: DefinitionId) -> Option<u32> {
796        self.turn_counts.get(&id).copied().or_else(|| {
797            self.base
798                .as_deref()
799                .and_then(|b| b.chain_get_turn_count(id))
800        })
801    }
802
803    /// Chain-lookup the overridden turn index.
804    fn chain_get_turn_index(&self) -> Option<u32> {
805        self.turn_index.or_else(|| {
806            self.base
807                .as_deref()
808                .and_then(FrozenLocal::chain_get_turn_index)
809        })
810    }
811
812    /// Chain-lookup the overridden RNG stream.
813    fn chain_get_rng(&self) -> Option<LocalRng> {
814        self.rng
815            .or_else(|| self.base.as_deref().and_then(FrozenLocal::chain_get_rng))
816    }
817}
818
819/// Errors from [`commit`].
820///
821/// A single variant today: `commit` is a documented, deferred seam (see
822/// `docs/scoped-flow-state-spec.md`, "Write-back is determined by scope, not
823/// a separate knob") — this release ships fork + discard only.
824#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
825pub enum CommitError {
826    /// Folding a forked child's own-layer overrides back into its parent is
827    /// deferred past this release. Fork's only supported terminal operation
828    /// today is **discard** (drop the child); `commit` always returns this.
829    #[error(
830        "commit is not implemented in this release; fork's only supported terminal operation is discard (drop the child)"
831    )]
832    NotImplemented,
833}
834
835/// Fold a forked child's own-layer overrides back into its parent's,
836/// making the child's writes visible through `parent` (and, transitively,
837/// anything `parent` itself chains to or is later written through).
838///
839/// **This is a deferred seam, not implemented in this release.** It exists
840/// so the shape of the eventual write-back API is fixed and callers can
841/// write code against it now (always getting `CommitError::NotImplemented`
842/// back) rather than the API appearing later with no forward-compatible
843/// slot. Per the spec, only a **fork** ever commits — a root flow has no
844/// parent to fold into, and its `Local`-scoped writes already persist for
845/// its own lifetime; `World`-scoped writes already escape live, with no
846/// "make it back" step needed.
847///
848/// Intended semantics, when implemented: walk `child`'s own top-layer
849/// overrides (globals, visit counts, turn counts, turn index, RNG) and
850/// apply each onto `parent`'s own top layer, as if `child`'s writes had
851/// been made directly against `parent` — last-write-wins per unit, since a
852/// fork is single-writer for its whole lifetime (no concurrent-write
853/// conflict is possible, so no merge policy is needed). Commit never
854/// touches `World` directly: a folded `Local`-scoped write still only
855/// lands in `parent`'s overrides, reaching `World` only if `parent` is
856/// itself later written through a `World`-scoped op or committed further up
857/// the chain. `Sandbox`-mode writes are exactly what this would fold
858/// back — commit is what would turn a sandboxed probe into a real,
859/// persisted mutation, for a caller that chooses to call it instead of
860/// dropping the child.
861///
862/// # Errors
863///
864/// Always returns [`CommitError::NotImplemented`] in this release.
865pub fn commit(_child: FlowLocal, _parent: &mut FlowLocal) -> Result<(), CommitError> {
866    Err(CommitError::NotImplemented)
867}
868
869/// Routing view implementing [`ContextAccess`] over `(&mut World, &mut
870/// FlowLocal)`.
871///
872/// This is what the VM's drive path receives as its `impl ContextAccess`.
873/// Every op computes an **effective scope** for its unit — see
874/// [`ContextView::effective_scope`] — and then routes exactly as before:
875///
876/// - **`World`-scoped**: always routes straight to `World` — reads and
877///   writes are immediately visible to every flow sharing that `World`.
878/// - **`Local`-scoped, read**: **chain read-through** — walks the
879///   `FlowLocal`'s own overrides, then its frozen `base` (recursively, see
880///   [`FlowLocal::chain_get_global`] and friends), then falls back to
881///   `World`'s current value on a total miss (so a flow that has never
882///   written a Local unit, nor inherited one from a base, sees the shared
883///   default until its first local write).
884/// - **`Local`-scoped, write**: lands in the `FlowLocal`'s own top-layer
885///   overrides only; `World` (and any frozen `base`) is untouched.
886/// - **`Local`-scoped, increment** (`increment_visit`,
887///   `increment_turn_index`): copy-on-write from the *chain read-through*
888///   value — read the current value (own override, else base chain, else
889///   World fallback), add one, store the result as the new top-layer
890///   override. This is what makes a flow's first local increment start
891///   from the chain's (or World's) count rather than 0.
892///
893/// **The effective scope, not the raw policy scope, drives all of the
894/// above.** In [`Mode::Normal`] (every construction path before F3.2, and
895/// every non-forked flow today) the effective scope of a unit *is* its
896/// `ResolvedPolicy` scope — unchanged from F2.2/F3.1. In [`Mode::Sandbox`]
897/// the effective scope of **every** unit is `Local`, no matter what the
898/// policy says: a sandboxed flow's reads still chain-read-through to
899/// `World`'s live value on a miss (so it observes current shared state),
900/// but its writes — including to units the policy homes to `World` — land
901/// only in its own `FlowLocal` overrides. `World` is therefore a read-only
902/// base from a sandboxed flow's perspective: nothing it does can mutate the
903/// shared world.
904///
905/// Because the all-`World` policy (the only policy the oracle corpus
906/// exercises) takes the `World` branch on every op *and* no existing
907/// construction path ever sets `Mode::Sandbox`, this is byte-identical to
908/// the F1.3 all-`World` passthrough for every existing single-flow
909/// construction path.
910pub struct ContextView<'a> {
911    world: &'a mut World,
912    local: &'a mut FlowLocal,
913}
914
915impl<'a> ContextView<'a> {
916    /// Build a routing view over a `World` and `FlowLocal` pair for the
917    /// duration of one step.
918    pub fn new(world: &'a mut World, local: &'a mut FlowLocal) -> Self {
919        Self { world, local }
920    }
921
922    /// The scope a unit actually routes by: `policy_scope` in
923    /// [`Mode::Normal`], or unconditionally [`Scope::Local`] in
924    /// [`Mode::Sandbox`] — see the type docs above.
925    #[inline]
926    fn effective_scope(&self, policy_scope: Scope) -> Scope {
927        match self.local.mode {
928            Mode::Normal => policy_scope,
929            Mode::Sandbox => Scope::Local,
930        }
931    }
932}
933
934impl ContextAccess for World {
935    #[inline]
936    fn global(&self, idx: u32) -> &Value {
937        &self.globals[idx as usize]
938    }
939
940    #[inline]
941    fn set_global(&mut self, idx: u32, value: Value) {
942        self.globals[idx as usize] = value;
943    }
944
945    /// Real move via [`core::mem::replace`] — `World`'s globals are a flat
946    /// `Vec<Value>`, so taking is exactly as cheap as an ordinary indexed
947    /// write, with no extra `Arc` clone (unlike the trait's default
948    /// clone-then-null implementation).
949    #[inline]
950    fn take_global(&mut self, idx: u32) -> Value {
951        core::mem::replace(&mut self.globals[idx as usize], Value::Null)
952    }
953
954    #[inline]
955    fn visit_count(&self, id: DefinitionId) -> u32 {
956        self.visit_counts.get(&id).copied().unwrap_or(0)
957    }
958
959    #[inline]
960    fn increment_visit(&mut self, id: DefinitionId) {
961        *self.visit_counts.entry(id).or_insert(0) += 1;
962    }
963
964    #[inline]
965    fn set_visit_count(&mut self, id: DefinitionId, count: u32) {
966        self.visit_counts.insert(id, count);
967    }
968
969    #[inline]
970    fn turn_count(&self, id: DefinitionId) -> Option<u32> {
971        self.turn_counts.get(&id).copied()
972    }
973
974    #[inline]
975    fn set_turn_count(&mut self, id: DefinitionId, turn: u32) {
976        self.turn_counts.insert(id, turn);
977    }
978
979    #[inline]
980    fn turn_index(&self) -> u32 {
981        self.turn_index
982    }
983
984    #[inline]
985    fn increment_turn_index(&mut self) {
986        self.turn_index += 1;
987    }
988
989    #[inline]
990    fn set_turn_index(&mut self, index: u32) {
991        self.turn_index = index;
992    }
993
994    #[inline]
995    fn rng_seed(&self) -> i32 {
996        self.rng_seed
997    }
998
999    #[inline]
1000    fn set_rng_seed(&mut self, seed: i32) {
1001        self.rng_seed = seed;
1002    }
1003
1004    #[inline]
1005    fn previous_random(&self) -> i32 {
1006        self.previous_random
1007    }
1008
1009    #[inline]
1010    fn set_previous_random(&mut self, val: i32) {
1011        self.previous_random = val;
1012    }
1013
1014    #[inline]
1015    fn next_random<R: StoryRng>(&self, seed: i32) -> i32 {
1016        let mut rng = R::from_seed(seed);
1017        rng.next_int()
1018    }
1019
1020    fn random_sequence<R: StoryRng>(&self, seed: i32, count: usize) -> Vec<i32> {
1021        let mut rng = R::from_seed(seed);
1022        (0..count).map(|_| rng.next_int()).collect()
1023    }
1024}
1025
1026impl ContextAccess for ContextView<'_> {
1027    #[inline]
1028    fn global(&self, idx: u32) -> &Value {
1029        match self.effective_scope(self.world.policy.scope_of_global(idx)) {
1030            Scope::Local => self
1031                .local
1032                .chain_get_global(idx)
1033                .unwrap_or_else(|| self.world.global(idx)),
1034            Scope::World => self.world.global(idx),
1035        }
1036    }
1037
1038    #[inline]
1039    fn set_global(&mut self, idx: u32, value: Value) {
1040        match self.effective_scope(self.world.policy.scope_of_global(idx)) {
1041            Scope::Local => {
1042                self.local.globals.insert(idx, value);
1043            }
1044            Scope::World => self.world.set_global(idx, value),
1045        }
1046    }
1047
1048    /// `World`-scoped units (the all-`World` default policy every oracle
1049    /// program runs under) delegate straight to `World::take_global`'s real
1050    /// move. `Local`-scoped units use the trait's default clone-then-null:
1051    /// a real move only helps when *this* flow already owns the unique
1052    /// reference, which the read-through chain (own overrides → frozen
1053    /// base → `World`) can't generally provide — an immutable
1054    /// [`FrozenLocal`] ancestor can't be moved out of. Own-layer overrides
1055    /// (`self.local.globals`) *could* be moved out of directly, but the
1056    /// perf-critical path this closes (value-model-spec §5's loop-append
1057    /// cliff) is the common single-`World` case; a `Local`-scoped fast path
1058    /// is future work if profiling ever shows it matters (T1b-4/#576 scope
1059    /// note).
1060    #[inline]
1061    fn take_global(&mut self, idx: u32) -> Value {
1062        match self.effective_scope(self.world.policy.scope_of_global(idx)) {
1063            Scope::Local => {
1064                let v = self.global(idx).clone();
1065                self.local.globals.insert(idx, Value::Null);
1066                v
1067            }
1068            Scope::World => self.world.take_global(idx),
1069        }
1070    }
1071
1072    #[inline]
1073    fn visit_count(&self, id: DefinitionId) -> u32 {
1074        match self.effective_scope(self.world.policy.scope_of_knot(id)) {
1075            Scope::Local => self
1076                .local
1077                .chain_get_visit_count(id)
1078                .unwrap_or_else(|| self.world.visit_count(id)),
1079            Scope::World => self.world.visit_count(id),
1080        }
1081    }
1082
1083    #[inline]
1084    fn increment_visit(&mut self, id: DefinitionId) {
1085        match self.effective_scope(self.world.policy.scope_of_knot(id)) {
1086            Scope::Local => {
1087                let base = self.visit_count(id);
1088                self.local.visit_counts.insert(id, base + 1);
1089            }
1090            Scope::World => self.world.increment_visit(id),
1091        }
1092    }
1093
1094    #[inline]
1095    fn set_visit_count(&mut self, id: DefinitionId, count: u32) {
1096        match self.effective_scope(self.world.policy.scope_of_knot(id)) {
1097            Scope::Local => {
1098                self.local.visit_counts.insert(id, count);
1099            }
1100            Scope::World => self.world.set_visit_count(id, count),
1101        }
1102    }
1103
1104    #[inline]
1105    fn turn_count(&self, id: DefinitionId) -> Option<u32> {
1106        match self.effective_scope(self.world.policy.scope_of_knot(id)) {
1107            Scope::Local => self
1108                .local
1109                .chain_get_turn_count(id)
1110                .or_else(|| self.world.turn_count(id)),
1111            Scope::World => self.world.turn_count(id),
1112        }
1113    }
1114
1115    #[inline]
1116    fn set_turn_count(&mut self, id: DefinitionId, turn: u32) {
1117        match self.effective_scope(self.world.policy.scope_of_knot(id)) {
1118            Scope::Local => {
1119                self.local.turn_counts.insert(id, turn);
1120            }
1121            Scope::World => self.world.set_turn_count(id, turn),
1122        }
1123    }
1124
1125    #[inline]
1126    fn turn_index(&self) -> u32 {
1127        match self.effective_scope(self.world.policy.turn_index_scope()) {
1128            Scope::Local => self
1129                .local
1130                .chain_get_turn_index()
1131                .unwrap_or_else(|| self.world.turn_index()),
1132            Scope::World => self.world.turn_index(),
1133        }
1134    }
1135
1136    #[inline]
1137    fn increment_turn_index(&mut self) {
1138        match self.effective_scope(self.world.policy.turn_index_scope()) {
1139            Scope::Local => {
1140                let base = self.turn_index();
1141                self.local.turn_index = Some(base + 1);
1142            }
1143            Scope::World => self.world.increment_turn_index(),
1144        }
1145    }
1146
1147    #[inline]
1148    fn set_turn_index(&mut self, index: u32) {
1149        match self.effective_scope(self.world.policy.turn_index_scope()) {
1150            Scope::Local => {
1151                self.local.turn_index = Some(index);
1152            }
1153            Scope::World => self.world.set_turn_index(index),
1154        }
1155    }
1156
1157    #[inline]
1158    fn rng_seed(&self) -> i32 {
1159        match self.effective_scope(self.world.policy.rng_scope()) {
1160            Scope::Local => self
1161                .local
1162                .chain_get_rng()
1163                .map_or_else(|| self.world.rng_seed(), |rng| rng.seed),
1164            Scope::World => self.world.rng_seed(),
1165        }
1166    }
1167
1168    #[inline]
1169    fn set_rng_seed(&mut self, seed: i32) {
1170        match self.effective_scope(self.world.policy.rng_scope()) {
1171            Scope::Local => {
1172                // CoW from the chain read-through value: seed a fresh local
1173                // override from the base chain's RNG if present, else World.
1174                // (base is always None in F3.1, so this reduces to World.)
1175                let fallback = self.local.chain_get_rng().unwrap_or(LocalRng {
1176                    seed: self.world.rng_seed(),
1177                    previous_random: self.world.previous_random(),
1178                });
1179                let rng = self.local.rng.get_or_insert(fallback);
1180                rng.seed = seed;
1181            }
1182            Scope::World => self.world.set_rng_seed(seed),
1183        }
1184    }
1185
1186    #[inline]
1187    fn previous_random(&self) -> i32 {
1188        match self.effective_scope(self.world.policy.rng_scope()) {
1189            Scope::Local => self
1190                .local
1191                .chain_get_rng()
1192                .map_or_else(|| self.world.previous_random(), |rng| rng.previous_random),
1193            Scope::World => self.world.previous_random(),
1194        }
1195    }
1196
1197    #[inline]
1198    fn set_previous_random(&mut self, val: i32) {
1199        match self.effective_scope(self.world.policy.rng_scope()) {
1200            Scope::Local => {
1201                // CoW from the chain read-through value (see `set_rng_seed`).
1202                let fallback = self.local.chain_get_rng().unwrap_or(LocalRng {
1203                    seed: self.world.rng_seed(),
1204                    previous_random: self.world.previous_random(),
1205                });
1206                let rng = self.local.rng.get_or_insert(fallback);
1207                rng.previous_random = val;
1208            }
1209            Scope::World => self.world.set_previous_random(val),
1210        }
1211    }
1212
1213    #[inline]
1214    fn next_random<R: StoryRng>(&self, seed: i32) -> i32 {
1215        // Pure function of the explicit `seed` argument — not routed
1216        // state, so this delegates to `World`'s implementation unchanged.
1217        // The routed `rng_seed()` above is what call sites read to obtain
1218        // `seed` in the first place.
1219        self.world.next_random::<R>(seed)
1220    }
1221
1222    fn random_sequence<R: StoryRng>(&self, seed: i32, count: usize) -> Vec<i32> {
1223        self.world.random_sequence::<R>(seed, count)
1224    }
1225}
1226
1227// ── FrameStartView: borrow, don't copy ──────────────────────────────────
1228
1229/// A **borrowing** view over a pinned frame-start [`World`]: reads are
1230/// served by reference from the shared `&World`, writes land in a private
1231/// per-view overlay that the shared world never sees.
1232///
1233/// This is `docs/effects-spec.md` §12.2's "borrow, don't copy" primitive
1234/// (issue #937). It exists for batch-mode stepping, where N flows must each
1235/// advance against *the same* frame-start state while their writes stay
1236/// private until a later, ordered Apply pass. The obvious way to get that
1237/// is to hand every flow its own `frame_start.clone()`; the cost of that
1238/// clone is `O(world size)` **per flow, per turn** — every global `Value`,
1239/// every visit/turn-count entry — which is nearly free for a scalar toy
1240/// world and emphatically not free for a real game's.
1241///
1242/// `FrameStartView` pays `O(1)` to construct and `O(cells this flow
1243/// actually wrote)` thereafter. It is **observationally identical** to
1244/// stepping against a private clone:
1245///
1246/// - a **read** returns the overlay's value if this view has written that
1247///   cell, else the frame-start value — i.e. `frame_start ⊕ own writes`,
1248///   exactly what a clone would hold;
1249/// - a **write** only ever mutates the overlay, so the borrowed `&World`
1250///   (and therefore every peer view over it) is untouched;
1251/// - an **increment** (`increment_visit`, `increment_turn_index`) is
1252///   copy-on-write from that same read-through value, so a flow's first
1253///   increment starts from the frame-start count rather than 0.
1254///
1255/// Because it borrows shared-immutably, many `FrameStartView`s over one
1256/// `&World` can run **concurrently** — the property `bevy-brink`'s parallel
1257/// batch driver needs, and the reason this type takes `&World` rather than
1258/// the `&mut World` [`ContextView`] requires. The semantics are those of
1259/// [`Mode::Sandbox`] (every unit treated as flow-private, the shared world
1260/// read-only), reachable here without a `&mut` borrow and without a
1261/// [`FlowLocal`] chain — this view has no frozen base and consults no
1262/// [`ResolvedPolicy`], because every cell is unconditionally overlaid.
1263///
1264/// The overlay is intentionally **not** readable back out: the authoritative
1265/// record of what a flow wrote is the
1266/// [`WriteObserver`](crate::WriteObserver) callback stream, which a caller
1267/// gets by wrapping this view in an
1268/// [`ObservedContext`](crate::ObservedContext). Keeping one changeset
1269/// record instead of two is what makes the buffered-write Apply pass
1270/// trivially consistent with what the flow actually observed.
1271pub struct FrameStartView<'a> {
1272    /// The pinned, shared frame-start state. Never mutated.
1273    frame_start: &'a World,
1274    /// Globals this view has written, keyed by slot index.
1275    globals: BTreeMap<u32, Value>,
1276    /// Visit counts this view has written or incremented.
1277    visit_counts: BTreeMap<DefinitionId, u32>,
1278    /// Turn counts this view has written.
1279    turn_counts: BTreeMap<DefinitionId, u32>,
1280    /// Turn index, once this view has written or incremented it.
1281    turn_index: Option<u32>,
1282    /// RNG stream, once this view has written either half of it.
1283    rng: Option<LocalRng>,
1284}
1285
1286impl<'a> FrameStartView<'a> {
1287    /// Open a fresh view over `frame_start`. The overlay starts empty, so
1288    /// every read passes straight through to the borrowed world until this
1289    /// view writes the cell in question.
1290    #[must_use]
1291    pub fn new(frame_start: &'a World) -> Self {
1292        Self {
1293            frame_start,
1294            globals: BTreeMap::new(),
1295            visit_counts: BTreeMap::new(),
1296            turn_counts: BTreeMap::new(),
1297            turn_index: None,
1298            rng: None,
1299        }
1300    }
1301
1302    /// The overlaid RNG stream, seeded from the frame-start values on first
1303    /// write — the copy-on-write half of `set_rng_seed`/`set_previous_random`
1304    /// (the two scalars [`WorldPolicy::rng`] scopes as one unit, so writing
1305    /// either must capture both).
1306    #[inline]
1307    fn rng_mut(&mut self) -> &mut LocalRng {
1308        self.rng.get_or_insert(LocalRng {
1309            seed: self.frame_start.rng_seed,
1310            previous_random: self.frame_start.previous_random,
1311        })
1312    }
1313}
1314
1315impl ContextAccess for FrameStartView<'_> {
1316    #[inline]
1317    fn global(&self, idx: u32) -> &Value {
1318        self.globals
1319            .get(&idx)
1320            .unwrap_or_else(|| self.frame_start.global(idx))
1321    }
1322
1323    #[inline]
1324    fn set_global(&mut self, idx: u32, value: Value) {
1325        self.globals.insert(idx, value);
1326    }
1327
1328    /// A real [`core::mem::replace`] move once the slot is in the overlay —
1329    /// which is what keeps `docs/value-model-spec.md` §5's take → `make_mut`
1330    /// → write-back discipline `O(1)`-amortized here, exactly as it is
1331    /// against a private clone. The **first** take of a given slot must
1332    /// still clone out of the borrowed frame-start world (it is shared; this
1333    /// view may not move out of it), but that is one `Arc` bump for one
1334    /// cell — the same bump a whole-world clone would have paid for that
1335    /// cell up front, and every subsequent take of the slot is a move.
1336    #[inline]
1337    fn take_global(&mut self, idx: u32) -> Value {
1338        if let Some(slot) = self.globals.get_mut(&idx) {
1339            return core::mem::replace(slot, Value::Null);
1340        }
1341        let value = self.frame_start.global(idx).clone();
1342        self.globals.insert(idx, Value::Null);
1343        value
1344    }
1345
1346    #[inline]
1347    fn visit_count(&self, id: DefinitionId) -> u32 {
1348        self.visit_counts
1349            .get(&id)
1350            .copied()
1351            .unwrap_or_else(|| self.frame_start.visit_count(id))
1352    }
1353
1354    #[inline]
1355    fn increment_visit(&mut self, id: DefinitionId) {
1356        let base = self.visit_count(id);
1357        self.visit_counts.insert(id, base + 1);
1358    }
1359
1360    #[inline]
1361    fn set_visit_count(&mut self, id: DefinitionId, count: u32) {
1362        self.visit_counts.insert(id, count);
1363    }
1364
1365    #[inline]
1366    fn turn_count(&self, id: DefinitionId) -> Option<u32> {
1367        self.turn_counts
1368            .get(&id)
1369            .copied()
1370            .or_else(|| self.frame_start.turn_count(id))
1371    }
1372
1373    #[inline]
1374    fn set_turn_count(&mut self, id: DefinitionId, turn: u32) {
1375        self.turn_counts.insert(id, turn);
1376    }
1377
1378    #[inline]
1379    fn turn_index(&self) -> u32 {
1380        self.turn_index.unwrap_or(self.frame_start.turn_index)
1381    }
1382
1383    #[inline]
1384    fn increment_turn_index(&mut self) {
1385        self.turn_index = Some(self.turn_index() + 1);
1386    }
1387
1388    #[inline]
1389    fn set_turn_index(&mut self, index: u32) {
1390        self.turn_index = Some(index);
1391    }
1392
1393    #[inline]
1394    fn rng_seed(&self) -> i32 {
1395        self.rng.map_or(self.frame_start.rng_seed, |rng| rng.seed)
1396    }
1397
1398    #[inline]
1399    fn set_rng_seed(&mut self, seed: i32) {
1400        self.rng_mut().seed = seed;
1401    }
1402
1403    #[inline]
1404    fn previous_random(&self) -> i32 {
1405        self.rng
1406            .map_or(self.frame_start.previous_random, |rng| rng.previous_random)
1407    }
1408
1409    #[inline]
1410    fn set_previous_random(&mut self, val: i32) {
1411        self.rng_mut().previous_random = val;
1412    }
1413
1414    #[inline]
1415    fn next_random<R: StoryRng>(&self, seed: i32) -> i32 {
1416        // Pure function of the explicit `seed` argument — not overlaid
1417        // state, so it delegates unchanged (see `ContextView`'s note).
1418        self.frame_start.next_random::<R>(seed)
1419    }
1420
1421    fn random_sequence<R: StoryRng>(&self, seed: i32, count: usize) -> Vec<i32> {
1422        self.frame_start.random_sequence::<R>(seed, count)
1423    }
1424}
1425
1426#[cfg(test)]
1427mod policy_tests {
1428    use super::*;
1429    use crate::link;
1430
1431    /// Compile a small ink story with the brink compiler and link it, for
1432    /// resolving policies against a real `Program` symbol table.
1433    fn compile(src: &str) -> Program {
1434        let out = brink_compiler::compile("t.ink", |p| {
1435            if p == "t.ink" {
1436                Ok(src.to_string())
1437            } else {
1438                Err(std::io::Error::new(
1439                    std::io::ErrorKind::NotFound,
1440                    "no such include",
1441                ))
1442            }
1443        })
1444        .expect("compile");
1445        let (program, _line_tables) = link(&out.data).expect("link");
1446        program
1447    }
1448
1449    fn sample_program() -> Program {
1450        compile(
1451            "VAR gold = 0\n\
1452             VAR mood = 0\n\
1453             -> shrine\n\
1454             === shrine ===\n\
1455             At the shrine.\n\
1456             -> END\n\
1457             === cellar ===\n\
1458             In the cellar.\n\
1459             -> END\n",
1460        )
1461    }
1462
1463    /// The default `WorldPolicy` (all-`World`) must resolve via the fast
1464    /// path — no name lookups — and every scope must read back as `World`.
1465    #[test]
1466    fn all_world_default_resolves_via_fast_path() {
1467        let program = sample_program();
1468        let policy = WorldPolicy::default();
1469
1470        let resolved = ResolvedPolicy::resolve(&program, &policy).expect("resolves");
1471
1472        // Fast path: no per-slot table populated.
1473        assert!(resolved.global_scopes.is_empty());
1474        assert!(resolved.knot_scopes.is_empty());
1475
1476        let gold_slot = program.global_index("gold").expect("gold declared");
1477        let mood_slot = program.global_index("mood").expect("mood declared");
1478        let shrine_id = program.find_path_target("shrine").expect("shrine exists");
1479        let cellar_id = program.find_path_target("cellar").expect("cellar exists");
1480
1481        assert_eq!(resolved.scope_of_global(gold_slot), Scope::World);
1482        assert_eq!(resolved.scope_of_global(mood_slot), Scope::World);
1483        assert_eq!(resolved.scope_of_knot(shrine_id), Scope::World);
1484        assert_eq!(resolved.scope_of_knot(cellar_id), Scope::World);
1485        assert_eq!(resolved.turn_index_scope(), Scope::World);
1486        assert_eq!(resolved.rng_scope(), Scope::World);
1487    }
1488
1489    /// A policy with `default: Local` plus explicit variable and knot
1490    /// overrides resolves each override to its named scope and leaves
1491    /// everything else at the default.
1492    #[test]
1493    fn resolves_valid_variable_and_knot_overrides() {
1494        let program = sample_program();
1495        let mut overrides = BTreeMap::new();
1496        overrides.insert("gold".to_owned(), Scope::World);
1497        overrides.insert("shrine".to_owned(), Scope::World);
1498        let policy = WorldPolicy {
1499            default: Scope::Local,
1500            overrides,
1501            turn_index: Scope::Local,
1502            rng: Scope::Local,
1503        };
1504
1505        let resolved = ResolvedPolicy::resolve(&program, &policy).expect("resolves");
1506
1507        let gold_slot = program.global_index("gold").expect("gold declared");
1508        let mood_slot = program.global_index("mood").expect("mood declared");
1509        let shrine_id = program.find_path_target("shrine").expect("shrine exists");
1510        let cellar_id = program.find_path_target("cellar").expect("cellar exists");
1511
1512        // Named overrides win.
1513        assert_eq!(resolved.scope_of_global(gold_slot), Scope::World);
1514        assert_eq!(resolved.scope_of_knot(shrine_id), Scope::World);
1515
1516        // Unlisted variable/knot fall back to `default`.
1517        assert_eq!(resolved.scope_of_global(mood_slot), Scope::Local);
1518        assert_eq!(resolved.scope_of_knot(cellar_id), Scope::Local);
1519
1520        // Scalars resolve independently of `overrides`.
1521        assert_eq!(resolved.turn_index_scope(), Scope::Local);
1522        assert_eq!(resolved.rng_scope(), Scope::Local);
1523    }
1524
1525    /// A name in `overrides` that matches neither a declared variable nor a
1526    /// resolvable knot/stitch path is a `PolicyError`, not a silent
1527    /// fallback to `default`.
1528    #[test]
1529    fn unknown_override_name_is_an_error() {
1530        let program = sample_program();
1531        let mut overrides = BTreeMap::new();
1532        overrides.insert("not_a_real_name".to_owned(), Scope::World);
1533        let policy = WorldPolicy {
1534            default: Scope::World,
1535            overrides,
1536            turn_index: Scope::World,
1537            rng: Scope::World,
1538        };
1539
1540        let err = ResolvedPolicy::resolve(&program, &policy).expect_err("must fail");
1541        assert_eq!(err, PolicyError::UnknownName("not_a_real_name".to_owned()));
1542    }
1543
1544    /// `World::new` resolves the policy and constructs a `World` whose
1545    /// globals match the program's declared defaults.
1546    #[test]
1547    fn world_new_resolves_policy_and_initializes_globals() {
1548        let program = sample_program();
1549        let world = World::new(&program, &WorldPolicy::default()).expect("world builds");
1550        assert_eq!(world.globals, program.global_defaults());
1551    }
1552
1553    /// `World::new` propagates the resolver's error for an unknown override
1554    /// name rather than panicking or silently ignoring it.
1555    #[test]
1556    fn world_new_propagates_unknown_name_error() {
1557        let program = sample_program();
1558        let mut overrides = BTreeMap::new();
1559        overrides.insert("nonexistent".to_owned(), Scope::Local);
1560        let policy = WorldPolicy {
1561            overrides,
1562            ..WorldPolicy::default()
1563        };
1564
1565        let err = World::new(&program, &policy).expect_err("must fail");
1566        assert_eq!(err, PolicyError::UnknownName("nonexistent".to_owned()));
1567    }
1568}
1569
1570#[cfg(test)]
1571mod routing_tests {
1572    use super::*;
1573    use crate::link;
1574
1575    /// Compile a small ink story with the brink compiler and link it, for
1576    /// resolving policies against a real `Program` symbol table.
1577    fn compile(src: &str) -> Program {
1578        let out = brink_compiler::compile("t.ink", |p| {
1579            if p == "t.ink" {
1580                Ok(src.to_string())
1581            } else {
1582                Err(std::io::Error::new(
1583                    std::io::ErrorKind::NotFound,
1584                    "no such include",
1585                ))
1586            }
1587        })
1588        .expect("compile");
1589        let (program, _line_tables) = link(&out.data).expect("link");
1590        program
1591    }
1592
1593    fn sample_program() -> Program {
1594        compile(
1595            "VAR gold = 0\n\
1596             VAR mood = 0\n\
1597             -> shrine\n\
1598             === shrine ===\n\
1599             At the shrine.\n\
1600             -> END\n\
1601             === cellar ===\n\
1602             In the cellar.\n\
1603             -> END\n",
1604        )
1605    }
1606
1607    /// `mood` is Local, `gold` stays World (the default). `shrine`/`cellar`
1608    /// are left at the World default too, so visit counts there stay
1609    /// shared — exercised separately below.
1610    fn mixed_policy() -> WorldPolicy {
1611        let mut overrides = BTreeMap::new();
1612        overrides.insert("mood".to_owned(), Scope::Local);
1613        WorldPolicy {
1614            default: Scope::World,
1615            overrides,
1616            turn_index: Scope::World,
1617            rng: Scope::World,
1618        }
1619    }
1620
1621    /// Writing a `Local`-scoped global in one flow must not affect another
1622    /// flow's `ContextView` over the same `World`, nor the `World` itself.
1623    /// Writing a `World`-scoped global in one flow must be immediately
1624    /// visible through another flow's `ContextView`.
1625    #[test]
1626    fn local_write_isolated_world_write_shared() {
1627        let program = sample_program();
1628        let policy = mixed_policy();
1629        let mut world = World::new(&program, &policy).expect("world builds");
1630
1631        let gold_slot = program.global_index("gold").expect("gold declared");
1632        let mood_slot = program.global_index("mood").expect("mood declared");
1633
1634        let mut local_a = FlowLocal::new();
1635        let mut local_b = FlowLocal::new();
1636
1637        // Flow A writes its Local `mood`.
1638        {
1639            let mut view_a = ContextView::new(&mut world, &mut local_a);
1640            view_a.set_global(mood_slot, Value::Int(42));
1641            assert_eq!(view_a.global(mood_slot), &Value::Int(42));
1642        }
1643
1644        // Flow B's view over the same World must not see A's local write —
1645        // it read-throughs to World's untouched default.
1646        {
1647            let view_b = ContextView::new(&mut world, &mut local_b);
1648            assert_eq!(view_b.global(mood_slot), &Value::Int(0));
1649            // World itself is untouched too.
1650            assert_eq!(world.global(mood_slot), &Value::Int(0));
1651        }
1652
1653        // Flow A writes its World-scoped `gold` — this must be immediately
1654        // visible via Flow B's view (and via World directly).
1655        {
1656            let mut view_a = ContextView::new(&mut world, &mut local_a);
1657            view_a.set_global(gold_slot, Value::Int(7));
1658        }
1659        {
1660            let view_b = ContextView::new(&mut world, &mut local_b);
1661            assert_eq!(view_b.global(gold_slot), &Value::Int(7));
1662        }
1663        assert_eq!(world.global(gold_slot), &Value::Int(7));
1664    }
1665
1666    /// Local visit counts increment independently per flow, while a
1667    /// World-scoped knot's visits are shared across flows.
1668    #[test]
1669    fn local_visits_independent_world_visits_shared() {
1670        let program = sample_program();
1671
1672        // shrine: Local: cellar stays at the World default.
1673        let mut overrides = BTreeMap::new();
1674        overrides.insert("shrine".to_owned(), Scope::Local);
1675        let policy = WorldPolicy {
1676            default: Scope::World,
1677            overrides,
1678            turn_index: Scope::World,
1679            rng: Scope::World,
1680        };
1681        let mut world = World::new(&program, &policy).expect("world builds");
1682
1683        let shrine_id = program.find_path_target("shrine").expect("shrine exists");
1684        let cellar_id = program.find_path_target("cellar").expect("cellar exists");
1685
1686        let mut local_a = FlowLocal::new();
1687        let mut local_b = FlowLocal::new();
1688
1689        // Flow A visits `shrine` (Local) twice.
1690        {
1691            let mut view_a = ContextView::new(&mut world, &mut local_a);
1692            view_a.increment_visit(shrine_id);
1693            view_a.increment_visit(shrine_id);
1694            assert_eq!(view_a.visit_count(shrine_id), 2);
1695        }
1696        // Flow B's local shrine count is independent — still 0.
1697        {
1698            let view_b = ContextView::new(&mut world, &mut local_b);
1699            assert_eq!(view_b.visit_count(shrine_id), 0);
1700        }
1701        // World's own bookkeeping for a Local-scoped knot is never touched.
1702        assert_eq!(world.visit_count(shrine_id), 0);
1703
1704        // `cellar` (World-scoped): Flow A's increment is visible to Flow B.
1705        {
1706            let mut view_a = ContextView::new(&mut world, &mut local_a);
1707            view_a.increment_visit(cellar_id);
1708        }
1709        {
1710            let view_b = ContextView::new(&mut world, &mut local_b);
1711            assert_eq!(view_b.visit_count(cellar_id), 1);
1712        }
1713        assert_eq!(world.visit_count(cellar_id), 1);
1714    }
1715
1716    /// A `Local`-scoped global reads through to World's current value until
1717    /// the flow performs its first local write.
1718    #[test]
1719    fn local_read_through_returns_world_default_before_first_write() {
1720        let program = sample_program();
1721        let policy = mixed_policy();
1722        let mut world = World::new(&program, &policy).expect("world builds");
1723        let mood_slot = program.global_index("mood").expect("mood declared");
1724
1725        // World's `mood` starts at the program default (0). A later World
1726        // write (e.g. host bootstrapping) should read through too, since A
1727        // hasn't written its own local override yet.
1728        world.set_global(mood_slot, Value::Int(99));
1729
1730        let mut local_a = FlowLocal::new();
1731        let view_a = ContextView::new(&mut world, &mut local_a);
1732        assert_eq!(view_a.global(mood_slot), &Value::Int(99));
1733    }
1734
1735    /// Local visit-count increment is copy-on-write from the read-through
1736    /// value: if World already has a nonzero count when Local is scoped
1737    /// in, the flow's first local increment starts from that base, not 0.
1738    #[test]
1739    fn local_increment_is_cow_from_read_through_base() {
1740        let program = sample_program();
1741        let mut overrides = BTreeMap::new();
1742        overrides.insert("shrine".to_owned(), Scope::Local);
1743        let policy = WorldPolicy {
1744            default: Scope::World,
1745            overrides,
1746            turn_index: Scope::World,
1747            rng: Scope::World,
1748        };
1749        let mut world = World::new(&program, &policy).expect("world builds");
1750        let shrine_id = program.find_path_target("shrine").expect("shrine exists");
1751
1752        // Seed World's bookkeeping directly (simulating pre-existing shared
1753        // state before this knot was scoped Local for this flow).
1754        world.increment_visit(shrine_id);
1755        world.increment_visit(shrine_id);
1756        assert_eq!(world.visit_count(shrine_id), 2);
1757
1758        let mut local_a = FlowLocal::new();
1759        let mut view_a = ContextView::new(&mut world, &mut local_a);
1760        // First local increment must start from World's base (2), not 0.
1761        view_a.increment_visit(shrine_id);
1762        assert_eq!(view_a.visit_count(shrine_id), 3);
1763
1764        // World's own count is untouched by the Local increment.
1765        assert_eq!(world.visit_count(shrine_id), 2);
1766    }
1767
1768    /// F3.1 chain read-through: a `FlowLocal` with a frozen `base` reads a
1769    /// value from the base when its own top layer has no override, and its
1770    /// own top-layer override shadows the base. Values that appear in
1771    /// neither layer still fall through to `World`.
1772    ///
1773    /// Built by hand (no fork exists yet — that's F3.2): freeze a parent
1774    /// `FlowLocal` that has some overrides, then attach that snapshot as a
1775    /// child's `base`.
1776    #[test]
1777    fn chain_read_through_reads_base_and_top_shadows() {
1778        let program = sample_program();
1779
1780        // Home both globals, both knots, turn index, and RNG to Local so the
1781        // chain (not World) is what's exercised on every read.
1782        let mut overrides = BTreeMap::new();
1783        overrides.insert("gold".to_owned(), Scope::Local);
1784        overrides.insert("mood".to_owned(), Scope::Local);
1785        overrides.insert("shrine".to_owned(), Scope::Local);
1786        overrides.insert("cellar".to_owned(), Scope::Local);
1787        let policy = WorldPolicy {
1788            default: Scope::World,
1789            overrides,
1790            turn_index: Scope::Local,
1791            rng: Scope::Local,
1792        };
1793        let mut world = World::new(&program, &policy).expect("world builds");
1794
1795        let gold_slot = program.global_index("gold").expect("gold declared");
1796        let mood_slot = program.global_index("mood").expect("mood declared");
1797        let shrine_id = program.find_path_target("shrine").expect("shrine exists");
1798        let cellar_id = program.find_path_target("cellar").expect("cellar exists");
1799
1800        // World defaults are distinct from anything we put in the chain, so a
1801        // read hitting World rather than the chain would be visible.
1802        world.set_global(gold_slot, Value::Int(1));
1803        world.set_global(mood_slot, Value::Int(1));
1804
1805        // Build a parent FlowLocal with overrides, then freeze it.
1806        let mut parent = FlowLocal::new();
1807        {
1808            let mut pv = ContextView::new(&mut world, &mut parent);
1809            pv.set_global(gold_slot, Value::Int(100));
1810            pv.set_global(mood_slot, Value::Int(200));
1811            pv.increment_visit(shrine_id); // parent shrine visit = 1
1812            pv.set_turn_count(cellar_id, 5);
1813            pv.increment_turn_index(); // parent turn index = 1
1814            pv.set_rng_seed(777);
1815        }
1816        let base = parent.freeze();
1817
1818        // Child inherits the frozen parent as its base, with its own empty
1819        // top layer.
1820        let mut child = FlowLocal {
1821            base: Some(base),
1822            ..FlowLocal::new()
1823        };
1824
1825        // Reads with an empty top layer see the base's values (not World's).
1826        {
1827            let view = ContextView::new(&mut world, &mut child);
1828            assert_eq!(view.global(gold_slot), &Value::Int(100));
1829            assert_eq!(view.global(mood_slot), &Value::Int(200));
1830            assert_eq!(view.visit_count(shrine_id), 1);
1831            assert_eq!(view.turn_count(cellar_id), Some(5));
1832            assert_eq!(view.turn_index(), 1);
1833            assert_eq!(view.rng_seed(), 777);
1834        }
1835
1836        // A top-layer override shadows the base for that one unit; other
1837        // units keep reading through to the base.
1838        {
1839            let mut view = ContextView::new(&mut world, &mut child);
1840            view.set_global(gold_slot, Value::Int(999));
1841            assert_eq!(view.global(gold_slot), &Value::Int(999)); // shadowed
1842            assert_eq!(view.global(mood_slot), &Value::Int(200)); // still base
1843        }
1844
1845        // A knot with no override anywhere in the chain falls through to
1846        // World (whose count for the Local-scoped `cellar` is untouched: 0).
1847        {
1848            let view = ContextView::new(&mut world, &mut child);
1849            assert_eq!(view.visit_count(cellar_id), 0);
1850        }
1851    }
1852
1853    /// F3.2 fork isolation (`Mode::Normal` child): forking a parent
1854    /// `FlowLocal` gives the child a frozen view of the parent's overrides
1855    /// at fork time. A write the child makes to a `Local`-scoped unit lands
1856    /// only in the child's own top layer — the parent's `FlowLocal` and
1857    /// `World` are both unaffected.
1858    #[test]
1859    fn fork_isolation_normal_child_write_does_not_leak_to_parent_or_world() {
1860        let program = sample_program();
1861        let policy = mixed_policy(); // mood: Local, gold: World (default)
1862        let mut world = World::new(&program, &policy).expect("world builds");
1863        let mood_slot = program.global_index("mood").expect("mood declared");
1864
1865        let mut parent = FlowLocal::new();
1866        {
1867            let mut view = ContextView::new(&mut world, &mut parent);
1868            view.set_global(mood_slot, Value::Int(42));
1869        }
1870
1871        // Fork a Normal child from the parent.
1872        let mut child = parent.fork(Mode::Normal);
1873
1874        // The child reads the parent's frozen state via `base` — it never
1875        // wrote `mood` itself.
1876        {
1877            let view = ContextView::new(&mut world, &mut child);
1878            assert_eq!(view.global(mood_slot), &Value::Int(42));
1879        }
1880
1881        // The child writes its own override for `mood`.
1882        {
1883            let mut view = ContextView::new(&mut world, &mut child);
1884            view.set_global(mood_slot, Value::Int(100));
1885            assert_eq!(view.global(mood_slot), &Value::Int(100));
1886        }
1887
1888        // The parent's own `FlowLocal` still reads its original write — the
1889        // child's write never reached it.
1890        {
1891            let view = ContextView::new(&mut world, &mut parent);
1892            assert_eq!(view.global(mood_slot), &Value::Int(42));
1893        }
1894
1895        // `World` was never touched — `mood` is Local-scoped, so it was
1896        // never written there in the first place.
1897        assert_eq!(world.global(mood_slot), &Value::Int(0));
1898    }
1899
1900    /// F3.2 frozen snapshot: a fork's `base` is a point-in-time snapshot.
1901    /// Mutating the parent *after* the fork must not be visible through the
1902    /// child, which still reads the parent's state as of the fork.
1903    #[test]
1904    fn fork_base_is_a_frozen_snapshot_later_parent_writes_invisible_to_child() {
1905        let program = sample_program();
1906        let policy = mixed_policy(); // mood: Local
1907        let mut world = World::new(&program, &policy).expect("world builds");
1908        let mood_slot = program.global_index("mood").expect("mood declared");
1909
1910        let mut parent = FlowLocal::new();
1911        {
1912            let mut view = ContextView::new(&mut world, &mut parent);
1913            view.set_global(mood_slot, Value::Int(1));
1914        }
1915
1916        let mut child = parent.fork(Mode::Normal);
1917
1918        // Mutate the parent *after* the fork.
1919        {
1920            let mut view = ContextView::new(&mut world, &mut parent);
1921            view.set_global(mood_slot, Value::Int(2));
1922        }
1923
1924        // The child's frozen base still reflects the pre-fork value.
1925        let view = ContextView::new(&mut world, &mut child);
1926        assert_eq!(view.global(mood_slot), &Value::Int(1));
1927    }
1928
1929    /// F3.2 sandbox side-effect-proof: in `Mode::Sandbox`, a `World`-scoped
1930    /// unit is still readable through the live `World` value, but any write
1931    /// — even to a unit the policy homes to `World` — lands only in the
1932    /// sandboxed flow's own overrides. `World` itself is never mutated, and
1933    /// dropping the sandboxed `FlowLocal` leaves no trace.
1934    #[test]
1935    fn sandbox_mode_writes_never_reach_world_reads_see_live_world() {
1936        let program = sample_program();
1937        let policy = mixed_policy(); // gold: World (default), mood: Local
1938        let mut world = World::new(&program, &policy).expect("world builds");
1939        let gold_slot = program.global_index("gold").expect("gold declared");
1940        let shrine_id = program.find_path_target("shrine").expect("shrine exists");
1941
1942        // Simulate pre-existing shared state the sandboxed flow should see.
1943        world.set_global(gold_slot, Value::Int(7));
1944        world.increment_visit(shrine_id);
1945        world.increment_visit(shrine_id);
1946        world.increment_visit(shrine_id);
1947        assert_eq!(world.visit_count(shrine_id), 3);
1948
1949        // Fork a sandboxed child from a "live" flow's (empty) FlowLocal.
1950        let live = FlowLocal::new();
1951        {
1952            let mut sandboxed = live.fork(Mode::Sandbox);
1953
1954            // Reads see World's current, live value even though `gold` and
1955            // `shrine` are World-scoped by policy.
1956            {
1957                let view = ContextView::new(&mut world, &mut sandboxed);
1958                assert_eq!(view.global(gold_slot), &Value::Int(7));
1959                assert_eq!(view.visit_count(shrine_id), 3);
1960            }
1961
1962            // Writing the World-scoped `gold` in the sandbox diverts to the
1963            // sandbox's own overrides — it does not touch `World`.
1964            {
1965                let mut view = ContextView::new(&mut world, &mut sandboxed);
1966                view.set_global(gold_slot, Value::Int(555));
1967                assert_eq!(view.global(gold_slot), &Value::Int(555)); // visible locally
1968            }
1969            assert_eq!(world.global(gold_slot), &Value::Int(7)); // World unchanged
1970
1971            // Incrementing a World-scoped visit count in the sandbox is
1972            // copy-on-write from the live World count, but the increment
1973            // itself stays local — World's count is untouched.
1974            {
1975                let mut view = ContextView::new(&mut world, &mut sandboxed);
1976                view.increment_visit(shrine_id);
1977                assert_eq!(view.visit_count(shrine_id), 4); // sandbox sees 4
1978            }
1979            assert_eq!(world.visit_count(shrine_id), 3); // World still 3
1980
1981            // Dropping `sandboxed` here (end of scope) is discard — nothing
1982            // escaped to World, so there is nothing to unwind.
1983        }
1984
1985        // World is still clean after the sandboxed child is gone.
1986        assert_eq!(world.global(gold_slot), &Value::Int(7));
1987        assert_eq!(world.visit_count(shrine_id), 3);
1988    }
1989}
1990
1991#[cfg(test)]
1992mod save_load_tests {
1993    use super::*;
1994    use crate::link;
1995    use crate::rng::FastRng;
1996    use crate::story::{FallbackHandler, FlowInstance};
1997    use crate::{load_state, save_state};
1998
1999    /// Compile a small ink story with the brink compiler and link it,
2000    /// keeping the line tables `FlowInstance::drive_to_terminal` needs.
2001    fn compile_for_flow(src: &str) -> (Program, Vec<Vec<brink_format::LineEntry>>) {
2002        let out = brink_compiler::compile("t.ink", |p| {
2003            if p == "t.ink" {
2004                Ok(src.to_string())
2005            } else {
2006                Err(std::io::Error::new(
2007                    std::io::ErrorKind::NotFound,
2008                    "no such include",
2009                ))
2010            }
2011        })
2012        .expect("compile");
2013        link(&out.data).expect("link")
2014    }
2015
2016    /// A scoped save/load roundtrip (F6.1b): `gold` (global) and `shrine`
2017    /// (knot) are policy-scoped `Local`; `silver` (global) stays `World`
2018    /// (the default). Driving the flow populates both layers; saving
2019    /// through the routing view captures effective values regardless of
2020    /// scope. Loading into a **fresh** `(World, FlowLocal)` pair through a
2021    /// fresh view must land each unit back in the layer its policy
2022    /// names — `Local` units in the new `FlowLocal`'s override maps (the new
2023    /// `World`'s own copy stays untouched), `World` units directly in the
2024    /// new `World` (the new `FlowLocal` contributes nothing for them).
2025    #[test]
2026    fn scoped_save_load_lands_each_unit_in_its_policy_layer() {
2027        let (program, tables) = compile_for_flow(
2028            "VAR gold = 0\n\
2029             VAR silver = 0\n\
2030             ~ silver = 7\n\
2031             -> shrine\n\
2032             === shrine ===\n\
2033             ~ gold = 5\n\
2034             At the shrine.\n\
2035             -> DONE\n\
2036             === reader ===\n\
2037             {READ_COUNT(-> shrine)}\n\
2038             -> DONE\n",
2039            // `reader` is never entered — it exists only so the compiler's
2040            // counting-flags pass sees a visit-count read of `shrine` and
2041            // sets `CountingFlags::VISITS` on it (a knot whose visit count
2042            // is never read anywhere in the program has counting disabled
2043            // entirely — an existing compiler optimization).
2044        );
2045
2046        let mut overrides = BTreeMap::new();
2047        overrides.insert("gold".to_owned(), Scope::Local);
2048        overrides.insert("shrine".to_owned(), Scope::Local);
2049        let policy = WorldPolicy {
2050            default: Scope::World,
2051            overrides,
2052            turn_index: Scope::World,
2053            rng: Scope::World,
2054        };
2055
2056        let gold_slot = program.global_index("gold").expect("gold declared");
2057        let silver_slot = program.global_index("silver").expect("silver declared");
2058        let shrine_id = program.find_path_target("shrine").expect("shrine exists");
2059
2060        // Drive the flow against a World built from our policy — the
2061        // `FlowInstance::new_at_root`-returned World is discarded; only the
2062        // callstack/thread state it seeds matters here.
2063        let mut world = World::new(&program, &policy).expect("world builds");
2064        let mut local = FlowLocal::new();
2065        let save = {
2066            let (mut flow, _unused_default_world) = FlowInstance::new_at_root(&program);
2067            let mut view = ContextView::new(&mut world, &mut local);
2068            flow.drive_to_terminal::<FastRng>(&program, &tables, &mut view, &FallbackHandler, None)
2069                .expect("drive succeeds");
2070            save_state(&program, &view)
2071        };
2072
2073        assert_eq!(save.globals.get("gold"), Some(&Value::Int(5)));
2074        assert_eq!(save.globals.get("silver"), Some(&Value::Int(7)));
2075        assert_eq!(
2076            save.visits
2077                .iter()
2078                .find(|e| e.id == shrine_id)
2079                .map(|e| e.count),
2080            Some(1),
2081            "shrine should have a captured visit entry"
2082        );
2083
2084        // Load into a fresh (World, FlowLocal) pair, built from the same
2085        // policy but with none of the driven state.
2086        let mut world2 = World::new(&program, &policy).expect("world builds");
2087        let mut local2 = FlowLocal::new();
2088        let report = {
2089            let mut view2 = ContextView::new(&mut world2, &mut local2);
2090            load_state(&program, &mut view2, &save)
2091        };
2092        assert!(report.unknown_globals.is_empty(), "clean load: {report:?}");
2093
2094        // `gold` is Local-scoped: the load must land it in `local2`'s
2095        // override map, leaving `world2`'s own copy at its untouched
2096        // default. The routing view's effective read still sees 5.
2097        assert_eq!(
2098            world2.global(gold_slot),
2099            &Value::Int(0),
2100            "gold is Local-scoped; World's own copy must stay untouched"
2101        );
2102        {
2103            let view2 = ContextView::new(&mut world2, &mut local2);
2104            assert_eq!(view2.global(gold_slot), &Value::Int(5));
2105        }
2106
2107        // `silver` is World-scoped: the load must land it directly in
2108        // `world2`, readable without any FlowLocal involvement.
2109        assert_eq!(
2110            world2.global(silver_slot),
2111            &Value::Int(7),
2112            "silver is World-scoped; must land directly in World"
2113        );
2114
2115        // `shrine`'s visit count is Local-scoped: same split as `gold`.
2116        assert_eq!(
2117            world2.visit_count(shrine_id),
2118            0,
2119            "shrine is Local-scoped; World's own visit count must stay untouched"
2120        );
2121        {
2122            let view2 = ContextView::new(&mut world2, &mut local2);
2123            assert_eq!(view2.visit_count(shrine_id), 1);
2124        }
2125    }
2126}
2127
2128#[cfg(test)]
2129mod subtree_scope_tests {
2130    use super::*;
2131    use crate::link;
2132    use crate::rng::FastRng;
2133    use crate::story::{FallbackHandler, FlowInstance, Step};
2134
2135    /// Compile a small ink story with the brink compiler and link it,
2136    /// keeping the line tables `FlowInstance::drive_to_terminal` needs.
2137    fn compile_for_flow(src: &str) -> (Program, Vec<Vec<brink_format::LineEntry>>) {
2138        let out = brink_compiler::compile("t.ink", |p| {
2139            if p == "t.ink" {
2140                Ok(src.to_string())
2141            } else {
2142                Err(std::io::Error::new(
2143                    std::io::ErrorKind::NotFound,
2144                    "no such include",
2145                ))
2146            }
2147        })
2148        .expect("compile");
2149        link(&out.data).expect("link")
2150    }
2151
2152    /// A knot `guard_talk` with its own top-level stopping sequence
2153    /// (`{ Halt! | Back again? }`) plus a stitch `guard_talk.inner` with its
2154    /// own stopping sequence, and an unrelated `other_knot` — the minimal
2155    /// shape needed to exercise interior-container containment, the knot→
2156    /// stitch cascade, and most-specific-wins precedence.
2157    fn story_with_stitch_and_sequences() -> (Program, Vec<Vec<brink_format::LineEntry>>) {
2158        compile_for_flow(
2159            "VAR gold = 0\n\
2160             -> guard_talk\n\
2161             === guard_talk ===\n\
2162             { stopping: Halt! | Back again? }\n\
2163             -> inner\n\
2164             = inner\n\
2165             { stopping: A | B | C }\n\
2166             -> DONE\n\
2167             === other_knot ===\n\
2168             Other.\n\
2169             -> DONE\n",
2170        )
2171    }
2172
2173    /// Find the `DefinitionId` of the (single) interior container directly
2174    /// owned by `scope_owner` that carries `CountingFlags::VISITS` — i.e.
2175    /// the anonymous sequence container `handle_sequence` (`vm.rs`) keys its
2176    /// counter off. Panics if there isn't exactly one, since every test
2177    /// story here is built with exactly one stopping sequence per scope.
2178    fn find_owned_sequence_id(program: &Program, scope_owner: DefinitionId) -> DefinitionId {
2179        let mut found = None;
2180        for (idx, container) in program.containers.iter().enumerate() {
2181            #[expect(clippy::cast_possible_truncation, reason = "test fixture")]
2182            let idx = idx as u32;
2183            let owner = program.scope_ids[program.scope_table_idx(idx) as usize];
2184            if owner == scope_owner
2185                && container
2186                    .counting_flags
2187                    .contains(brink_format::CountingFlags::VISITS)
2188            {
2189                assert!(
2190                    found.is_none(),
2191                    "expected exactly one VISITS-counted interior container owned by {scope_owner:?}"
2192                );
2193                found = Some(container.id);
2194            }
2195        }
2196        found.expect("expected a VISITS-counted interior container")
2197    }
2198
2199    /// A knot marked `Local` must cover not just its own `DefinitionId` but
2200    /// the interior sequence container nested directly under it — this is
2201    /// the exact bug the F6 AMENDMENT (ruling 3) describes:
2202    /// `handle_sequence` keys a stopping/cycle counter off the sequence's
2203    /// *own* container id, not the enclosing knot's, so without subtree
2204    /// expansion a `Local`-marked knot would silently leave that counter
2205    /// `World`-scoped.
2206    #[test]
2207    fn marked_local_knot_covers_its_interior_sequence_container() {
2208        let (program, _tables) = story_with_stitch_and_sequences();
2209        let guard_talk_id = program
2210            .find_path_target("guard_talk")
2211            .expect("guard_talk exists");
2212        let other_knot_id = program
2213            .find_path_target("other_knot")
2214            .expect("other_knot exists");
2215        let sequence_id = find_owned_sequence_id(&program, guard_talk_id);
2216
2217        let mut overrides = BTreeMap::new();
2218        overrides.insert("guard_talk".to_owned(), Scope::Local);
2219        let policy = WorldPolicy {
2220            default: Scope::World,
2221            overrides,
2222            turn_index: Scope::World,
2223            rng: Scope::World,
2224        };
2225        let resolved = ResolvedPolicy::resolve(&program, &policy).expect("resolves");
2226
2227        assert_eq!(resolved.scope_of_knot(guard_talk_id), Scope::Local);
2228        assert_eq!(
2229            resolved.scope_of_knot(sequence_id),
2230            Scope::Local,
2231            "the interior sequence container must inherit guard_talk's Local scope"
2232        );
2233        // An unrelated knot must stay at the World default — the expansion
2234        // must not leak scope onto unrelated containers.
2235        assert_eq!(resolved.scope_of_knot(other_knot_id), Scope::World);
2236    }
2237
2238    /// Stitch-level override + most-specific-wins precedence: knot
2239    /// `guard_talk` is `Local`, but its stitch `guard_talk.inner` is
2240    /// explicitly `World`. Every container under `inner` (the stitch
2241    /// itself, and its own interior sequence) must resolve `World`; the
2242    /// rest of `guard_talk`'s subtree (the knot's own id and its own
2243    /// interior sequence) must resolve `Local`.
2244    #[test]
2245    fn stitch_override_wins_over_enclosing_knot_for_its_own_subtree() {
2246        let (program, _tables) = story_with_stitch_and_sequences();
2247        let guard_talk_id = program
2248            .find_path_target("guard_talk")
2249            .expect("guard_talk exists");
2250        let inner_id = program
2251            .find_path_target("guard_talk.inner")
2252            .expect("guard_talk.inner exists");
2253        let guard_talk_sequence_id = find_owned_sequence_id(&program, guard_talk_id);
2254        let inner_sequence_id = find_owned_sequence_id(&program, inner_id);
2255
2256        let mut overrides = BTreeMap::new();
2257        overrides.insert("guard_talk".to_owned(), Scope::Local);
2258        overrides.insert("guard_talk.inner".to_owned(), Scope::World);
2259        let policy = WorldPolicy {
2260            default: Scope::World,
2261            overrides,
2262            turn_index: Scope::World,
2263            rng: Scope::World,
2264        };
2265        let resolved = ResolvedPolicy::resolve(&program, &policy).expect("resolves");
2266
2267        assert_eq!(resolved.scope_of_knot(guard_talk_id), Scope::Local);
2268        assert_eq!(resolved.scope_of_knot(guard_talk_sequence_id), Scope::Local);
2269        assert_eq!(
2270            resolved.scope_of_knot(inner_id),
2271            Scope::World,
2272            "the stitch's own explicit override must win over its enclosing knot's"
2273        );
2274        assert_eq!(
2275            resolved.scope_of_knot(inner_sequence_id),
2276            Scope::World,
2277            "the stitch's interior sequence must follow the stitch's own override, \
2278             not the enclosing knot's"
2279        );
2280
2281        // And the reverse precedence: knot World (default), stitch Local —
2282        // confirms precedence isn't just "whichever happens to be Local".
2283        let mut overrides2 = BTreeMap::new();
2284        overrides2.insert("guard_talk".to_owned(), Scope::World);
2285        overrides2.insert("guard_talk.inner".to_owned(), Scope::Local);
2286        let policy2 = WorldPolicy {
2287            default: Scope::World,
2288            overrides: overrides2,
2289            turn_index: Scope::World,
2290            rng: Scope::World,
2291        };
2292        let resolved2 = ResolvedPolicy::resolve(&program, &policy2).expect("resolves");
2293        assert_eq!(resolved2.scope_of_knot(guard_talk_id), Scope::World);
2294        assert_eq!(
2295            resolved2.scope_of_knot(guard_talk_sequence_id),
2296            Scope::World
2297        );
2298        assert_eq!(resolved2.scope_of_knot(inner_id), Scope::Local);
2299        assert_eq!(resolved2.scope_of_knot(inner_sequence_id), Scope::Local);
2300    }
2301
2302    /// The all-`World` default policy must still resolve via
2303    /// `ResolvedPolicy::all_world`'s fast path — no per-slot/per-knot tables
2304    /// populated — even against a program with stitches and sequences that
2305    /// would otherwise drive subtree expansion. This is the oracle-anchored
2306    /// path every existing single-flow construction path takes; it must
2307    /// stay byte-identical.
2308    #[test]
2309    fn all_world_default_still_takes_fast_path() {
2310        let (program, _tables) = story_with_stitch_and_sequences();
2311        let resolved =
2312            ResolvedPolicy::resolve(&program, &WorldPolicy::default()).expect("resolves");
2313
2314        // Fast path: no per-slot/per-knot table populated, matching
2315        // `all_world()` exactly.
2316        assert!(resolved.global_scopes.is_empty());
2317        assert!(resolved.knot_scopes.is_empty());
2318
2319        let guard_talk_id = program
2320            .find_path_target("guard_talk")
2321            .expect("guard_talk exists");
2322        assert_eq!(resolved.scope_of_knot(guard_talk_id), Scope::World);
2323    }
2324
2325    /// An override name that resolves to neither a global nor a knot/
2326    /// stitch path is still a `PolicyError::UnknownName` — subtree
2327    /// expansion must not swallow or change this error path.
2328    #[test]
2329    fn unknown_override_name_still_errors() {
2330        let (program, _tables) = story_with_stitch_and_sequences();
2331        let mut overrides = BTreeMap::new();
2332        overrides.insert("guard_talk.nonexistent_stitch".to_owned(), Scope::Local);
2333        let policy = WorldPolicy {
2334            default: Scope::World,
2335            overrides,
2336            turn_index: Scope::World,
2337            rng: Scope::World,
2338        };
2339        let err = ResolvedPolicy::resolve(&program, &policy).expect_err("must fail");
2340        assert_eq!(
2341            err,
2342            PolicyError::UnknownName("guard_talk.nonexistent_stitch".to_owned())
2343        );
2344    }
2345
2346    /// End-to-end (F6.1c's motivating "per-entity memory" case): two
2347    /// `FlowInstance`s, each with its own `FlowLocal`, drive the *same*
2348    /// `guard_talk` knot (a stopping sequence, `{ Halt! | Back again? }`)
2349    /// over one shared `World` whose policy marks `guard_talk` `Local`.
2350    /// Without subtree expansion, the sequence's own interior container id
2351    /// isn't in `knot_scopes`, falls through to the `World` default, and
2352    /// the two flows' visits collide on one shared counter — the second
2353    /// flow would see "Back again?" on its very first encounter. With the
2354    /// fix, each flow's first encounter is independently the first-visit
2355    /// text.
2356    #[test]
2357    fn two_flows_over_shared_world_each_see_first_visit_text() {
2358        let (program, tables) = compile_for_flow(
2359            "VAR gold = 0\n\
2360             -> guard_talk\n\
2361             === guard_talk ===\n\
2362             { stopping: Halt! | Back again? }\n\
2363             -> DONE\n",
2364        );
2365
2366        let mut overrides = BTreeMap::new();
2367        overrides.insert("guard_talk".to_owned(), Scope::Local);
2368        let policy = WorldPolicy {
2369            default: Scope::World,
2370            overrides,
2371            turn_index: Scope::World,
2372            rng: Scope::World,
2373        };
2374        let mut world = World::new(&program, &policy).expect("world builds");
2375
2376        let drive = |flow: &mut FlowInstance, view: &mut ContextView<'_>| -> String {
2377            let lines = flow
2378                .drive_to_terminal::<FastRng>(&program, &tables, view, &FallbackHandler, None)
2379                .expect("drive succeeds");
2380            assert!(
2381                matches!(lines.last(), Some(Step::Done)),
2382                "expected Done, got {lines:?}"
2383            );
2384            lines.iter().map(Step::text).collect::<String>()
2385        };
2386
2387        // Flow A: first (and only, for this assertion) encounter.
2388        let (mut flow_a, _discarded_world_a) = FlowInstance::new_at_root(&program);
2389        let mut local_a = FlowLocal::new();
2390        let first_visit_a = {
2391            let mut view_a = ContextView::new(&mut world, &mut local_a);
2392            drive(&mut flow_a, &mut view_a)
2393        };
2394
2395        // Flow B: independent FlowLocal, same shared World. Its first
2396        // encounter must read exactly like Flow A's — not "already
2397        // visited" — proving the interior sequence container's visit count
2398        // is Local per-flow, not accidentally shared through World.
2399        let (mut flow_b, _discarded_world_b) = FlowInstance::new_at_root(&program);
2400        let mut local_b = FlowLocal::new();
2401        let first_visit_b = {
2402            let mut view_b = ContextView::new(&mut world, &mut local_b);
2403            drive(&mut flow_b, &mut view_b)
2404        };
2405
2406        assert_eq!(
2407            first_visit_a, first_visit_b,
2408            "both flows' first encounter with guard_talk must produce identical \
2409             (first-visit) text — each flow's visit count is independently local"
2410        );
2411
2412        // Flow A, re-entered a second time (still its own FlowLocal): now
2413        // it must progress to the *next* branch of the stopping sequence,
2414        // proving Local scoping still lets a single flow's own state
2415        // advance normally.
2416        let second_visit_a = {
2417            let mut view_a = ContextView::new(&mut world, &mut local_a);
2418            flow_a
2419                .choose_path_string(&program, &mut view_a, "guard_talk")
2420                .expect("re-enter guard_talk");
2421            drive(&mut flow_a, &mut view_a)
2422        };
2423        assert_ne!(
2424            first_visit_a, second_visit_a,
2425            "flow A's second encounter must progress past the first-visit branch"
2426        );
2427
2428        // Flow B's own local state must still be untouched by flow A's
2429        // second visit — driving B a second time reproduces A's *first*
2430        // progression, not A's second.
2431        let second_visit_b = {
2432            let mut view_b = ContextView::new(&mut world, &mut local_b);
2433            flow_b
2434                .choose_path_string(&program, &mut view_b, "guard_talk")
2435                .expect("re-enter guard_talk");
2436            drive(&mut flow_b, &mut view_b)
2437        };
2438        assert_eq!(
2439            second_visit_a, second_visit_b,
2440            "flow B's second encounter must match flow A's second encounter — \
2441             both progressed independently from the same (shared, untouched) \
2442             World default"
2443        );
2444
2445        // World's own bookkeeping for the Local-scoped knot must never have
2446        // been touched by either flow.
2447        let sequence_id = find_owned_sequence_id(&program, {
2448            program
2449                .find_path_target("guard_talk")
2450                .expect("guard_talk exists")
2451        });
2452        assert_eq!(
2453            world.visit_count(sequence_id),
2454            0,
2455            "World's own copy of the Local-scoped sequence's visit count must stay untouched"
2456        );
2457    }
2458}
2459
2460#[cfg(test)]
2461mod compiled_defaults_tests {
2462    //! Compiled `#@local` scope defaults seeding policy resolution
2463    //! (`docs/directive-annotations-spec.md` §4.6): the base layer of
2464    //! `base ⊕ host-overrides`, with zero host API involvement.
2465
2466    use std::collections::BTreeMap;
2467
2468    use super::*;
2469    use crate::link;
2470
2471    fn compile(src: &str) -> Program {
2472        let out = brink_compiler::compile("t.ink", |p| {
2473            if p == "t.ink" {
2474                Ok(src.to_string())
2475            } else {
2476                Err(std::io::Error::new(
2477                    std::io::ErrorKind::NotFound,
2478                    "no such include",
2479                ))
2480            }
2481        })
2482        .expect("compile");
2483        let (program, _line_tables) = link(&out.data).expect("link");
2484        program
2485    }
2486
2487    /// `mood` and `shrine` (with a stitch and an interior sequence) are
2488    /// marked `#@local` in source; `gold` and `cellar` stay unmarked.
2489    fn annotated_program() -> Program {
2490        compile(
2491            "VAR gold = 0\n\
2492             #@local\n\
2493             VAR mood = 0\n\
2494             -> shrine\n\
2495             === shrine ===\n\
2496             #@local\n\
2497             At the shrine {&once|again}.\n\
2498             = inner\n\
2499             Deeper in.\n\
2500             -> END\n\
2501             === cellar ===\n\
2502             In the cellar.\n\
2503             -> END\n",
2504        )
2505    }
2506
2507    #[test]
2508    fn unannotated_program_keeps_the_fast_path() {
2509        let program = compile("VAR gold = 0\nhello\n");
2510        assert!(!program.has_local_defaults());
2511        let resolved =
2512            ResolvedPolicy::resolve(&program, &WorldPolicy::default()).expect("resolves");
2513        // The all-World fast path allocates nothing.
2514        assert!(resolved.global_scopes.is_empty());
2515        assert!(resolved.knot_scopes.is_empty());
2516    }
2517
2518    #[test]
2519    fn compiled_local_var_seeds_the_base() {
2520        let program = annotated_program();
2521        assert!(program.has_local_defaults());
2522        let resolved =
2523            ResolvedPolicy::resolve(&program, &WorldPolicy::default()).expect("resolves");
2524
2525        let mood = program.global_index("mood").expect("mood declared");
2526        let gold = program.global_index("gold").expect("gold declared");
2527        assert_eq!(resolved.scope_of_global(mood), Scope::Local);
2528        assert_eq!(resolved.scope_of_global(gold), Scope::World);
2529    }
2530
2531    #[test]
2532    fn compiled_local_knot_covers_its_subtree() {
2533        let program = annotated_program();
2534        let resolved =
2535            ResolvedPolicy::resolve(&program, &WorldPolicy::default()).expect("resolves");
2536
2537        let shrine = program.find_path_target("shrine").expect("shrine exists");
2538        let inner = program
2539            .find_path_target("shrine.inner")
2540            .expect("stitch exists");
2541        let cellar = program.find_path_target("cellar").expect("cellar exists");
2542
2543        assert_eq!(resolved.scope_of_knot(shrine), Scope::Local);
2544        assert_eq!(
2545            resolved.scope_of_knot(inner),
2546            Scope::Local,
2547            "a #@local knot covers its stitches"
2548        );
2549        assert_eq!(resolved.scope_of_knot(cellar), Scope::World);
2550
2551        // Interior containers (the inline sequence) are covered too.
2552        let interior = interior_containers_by_scope(&program);
2553        let shrine_interior = interior.get(&shrine).cloned().unwrap_or_default();
2554        assert!(
2555            !shrine_interior.is_empty(),
2556            "the {{&…}} sequence creates interior containers under shrine"
2557        );
2558        for id in shrine_interior {
2559            assert_eq!(
2560                resolved.scope_of_knot(id),
2561                Scope::Local,
2562                "interior container {id:?} inherits the knot's compiled scope"
2563            );
2564        }
2565    }
2566
2567    #[test]
2568    fn host_override_beats_the_compiled_bit() {
2569        let program = annotated_program();
2570        let mut overrides = BTreeMap::new();
2571        overrides.insert("mood".to_owned(), Scope::World);
2572        overrides.insert("shrine".to_owned(), Scope::World);
2573        let policy = WorldPolicy {
2574            default: Scope::World,
2575            overrides,
2576            turn_index: Scope::World,
2577            rng: Scope::World,
2578        };
2579        let resolved = ResolvedPolicy::resolve(&program, &policy).expect("resolves");
2580
2581        let mood = program.global_index("mood").expect("mood declared");
2582        let shrine = program.find_path_target("shrine").expect("shrine exists");
2583        assert_eq!(
2584            resolved.scope_of_global(mood),
2585            Scope::World,
2586            "host override wins over the compiled #@local bit"
2587        );
2588        assert_eq!(resolved.scope_of_knot(shrine), Scope::World);
2589    }
2590
2591    /// End to end with zero host policy: a `World` built with the default
2592    /// (empty) `WorldPolicy` picks up the compiled bits, and two flows
2593    /// sharing it get isolated `mood` but shared `gold`.
2594    #[test]
2595    fn compiled_base_isolates_flows_without_host_policy() {
2596        let program = annotated_program();
2597        let mut world = World::new(&program, &WorldPolicy::default()).expect("world builds");
2598
2599        let mood = program.global_index("mood").expect("mood declared");
2600        let gold = program.global_index("gold").expect("gold declared");
2601
2602        let mut local_a = FlowLocal::new();
2603        let mut local_b = FlowLocal::new();
2604
2605        // Flow A writes both.
2606        {
2607            let mut view_a = ContextView::new(&mut world, &mut local_a);
2608            view_a.set_global(mood, Value::Int(42));
2609            view_a.set_global(gold, Value::Int(7));
2610        }
2611        // Flow B sees the shared `gold` but not A's private `mood`.
2612        {
2613            let view_b = ContextView::new(&mut world, &mut local_b);
2614            assert_eq!(view_b.global(mood), &Value::Int(0));
2615            assert_eq!(view_b.global(gold), &Value::Int(7));
2616        }
2617        assert_eq!(world.global(mood), &Value::Int(0));
2618
2619        // Visit counts: `shrine` is flow-private by compilation.
2620        let shrine = program.find_path_target("shrine").expect("shrine exists");
2621        {
2622            let mut view_a = ContextView::new(&mut world, &mut local_a);
2623            view_a.increment_visit(shrine);
2624            assert_eq!(view_a.visit_count(shrine), 1);
2625        }
2626        {
2627            let view_b = ContextView::new(&mut world, &mut local_b);
2628            assert_eq!(view_b.visit_count(shrine), 0);
2629        }
2630        assert_eq!(world.visit_count(shrine), 0);
2631    }
2632
2633    /// End to end through the VM: a `#@local` knot whose visit count is
2634    /// never *read* anywhere in the ink must still track visits — and
2635    /// track them per flow (#496). Without the compiler forcing
2636    /// `CountingFlags::VISITS` on marked containers, the read-site
2637    /// optimization compiles counting out and the VM never records the
2638    /// visit at all, in any layer.
2639    #[test]
2640    fn local_knot_with_no_reads_still_tracks_visits_per_flow() {
2641        use crate::rng::FastRng;
2642        use crate::story::{FallbackHandler, FlowInstance};
2643
2644        let out = brink_compiler::compile("t.ink", |p| {
2645            if p == "t.ink" {
2646                Ok("-> shrine\n\
2647                    === shrine ===\n\
2648                    #@local\n\
2649                    At the shrine.\n\
2650                    -> END\n"
2651                    .to_string())
2652            } else {
2653                Err(std::io::Error::new(
2654                    std::io::ErrorKind::NotFound,
2655                    "no such include",
2656                ))
2657            }
2658        })
2659        .expect("compile");
2660        let (program, tables) = link(&out.data).expect("link");
2661        let shrine = program.find_path_target("shrine").expect("shrine exists");
2662
2663        let mut world = World::new(&program, &WorldPolicy::default()).expect("world builds");
2664        let mut local_a = FlowLocal::new();
2665        let mut local_b = FlowLocal::new();
2666
2667        // Drive flow A through the knot.
2668        {
2669            let (mut flow, _unused_default_world) = FlowInstance::new_at_root(&program);
2670            let mut view_a = ContextView::new(&mut world, &mut local_a);
2671            flow.drive_to_terminal::<FastRng>(
2672                &program,
2673                &tables,
2674                &mut view_a,
2675                &FallbackHandler,
2676                None,
2677            )
2678            .expect("drive succeeds");
2679            assert_eq!(
2680                view_a.visit_count(shrine),
2681                1,
2682                "the VM must count the visit even though nothing reads it"
2683            );
2684        }
2685        // The count is flow-private: flow B and the shared World see 0.
2686        {
2687            let view_b = ContextView::new(&mut world, &mut local_b);
2688            assert_eq!(view_b.visit_count(shrine), 0);
2689        }
2690        assert_eq!(world.visit_count(shrine), 0);
2691    }
2692}
2693
2694/// `take_global` (issue #576, `docs/value-model-spec.md` §5) mechanics:
2695/// proves the move is real (not a disguised clone) using the same
2696/// `Arc::strong_count`/pointer-identity technique
2697/// `brink-format::value::tests` uses for `array_make_mut`'s COW proofs —
2698/// the load-bearing property behind this PR's O(1)-amortized loop-append
2699/// claim.
2700#[cfg(test)]
2701mod take_global_tests {
2702    use super::*;
2703
2704    fn world_with_one_global(value: Value) -> World {
2705        World::from_globals(vec![value], ResolvedPolicy::all_world())
2706    }
2707
2708    /// `World::take_global` is a real move: the returned value is the exact
2709    /// same `Arc` allocation an external clone already pointed at (not a
2710    /// fresh copy), the refcount doesn't go up because of the take, and the
2711    /// slot is left `Value::Null`.
2712    #[test]
2713    fn world_take_global_moves_without_cloning() {
2714        let array = Value::array(vec![Value::Int(1), Value::Int(2)]);
2715        let external = Arc::clone(array.as_array().expect("array"));
2716        assert_eq!(Arc::strong_count(&external), 2, "world's slot + external");
2717
2718        let mut world = world_with_one_global(array);
2719        let taken = world.take_global(0);
2720
2721        assert_eq!(
2722            Arc::as_ptr(taken.as_array().expect("array")),
2723            Arc::as_ptr(&external),
2724            "take_global must return the SAME allocation, not a copy"
2725        );
2726        assert_eq!(
2727            Arc::strong_count(&external),
2728            2,
2729            "the take itself must not bump the refcount: external + taken, \
2730             the world's own slot reference is GONE (moved out, not cloned)"
2731        );
2732        assert_eq!(
2733            world.global(0),
2734            &Value::Null,
2735            "the slot must be left Value::Null after a take"
2736        );
2737    }
2738
2739    /// Contrast with the ordinary `global()` read: cloning DOES bump the
2740    /// refcount — this is the exact COW cliff #576 closes (a `GetGlobal`
2741    /// clone leaves the slot AND the read both holding a reference, so a
2742    /// subsequent `array_make_mut` always sees itself as shared).
2743    #[test]
2744    fn ordinary_global_read_clones_and_bumps_refcount() {
2745        let array = Value::array(vec![Value::Int(1)]);
2746        let external = Arc::clone(array.as_array().expect("array"));
2747        assert_eq!(Arc::strong_count(&external), 2);
2748
2749        let world = world_with_one_global(array);
2750        let read = world.global(0).clone();
2751
2752        assert_eq!(
2753            Arc::strong_count(&external),
2754            3,
2755            "world's slot + external + this clone — the ordinary read path \
2756             genuinely bumps the refcount, unlike take_global"
2757        );
2758        drop(read);
2759    }
2760
2761    /// `ContextView` routes `take_global` straight to `World::take_global`
2762    /// (the real move) for `World`-scoped units — the common, oracle-anchored
2763    /// all-`World` policy every program runs under by default.
2764    #[test]
2765    fn context_view_world_scoped_take_is_a_real_move() {
2766        let array = Value::array(vec![Value::Int(7)]);
2767        let external = Arc::clone(array.as_array().expect("array"));
2768
2769        let mut world = world_with_one_global(array);
2770        let mut local = FlowLocal::new();
2771        let mut view = ContextView::new(&mut world, &mut local);
2772
2773        let taken = view.take_global(0);
2774        assert_eq!(
2775            Arc::as_ptr(taken.as_array().expect("array")),
2776            Arc::as_ptr(&external)
2777        );
2778        assert_eq!(Arc::strong_count(&external), 2, "no extra clone");
2779        assert_eq!(view.global(0), &Value::Null);
2780    }
2781
2782    /// `ContextView`'s `Local`-scoped branch (the trait default: clone then
2783    /// null) — correctness over a read-through miss: taking a `Local`-scoped
2784    /// global that's never been locally overridden reads `World`'s current
2785    /// value (via the read-through chain), leaves a `Value::Null` override
2786    /// in the flow's own layer, and never touches `World` itself.
2787    #[test]
2788    fn context_view_local_scoped_take_reads_through_and_nulls_local_override() {
2789        let array = Value::array(vec![Value::Int(9)]);
2790        let mut world = world_with_one_global(array.clone());
2791        // Force every global to Local scope.
2792        *world.policy = ResolvedPolicy {
2793            default: Scope::Local,
2794            global_scopes: vec![Scope::Local],
2795            knot_scopes: HashMap::new(),
2796            turn_index: Scope::World,
2797            rng: Scope::World,
2798        };
2799        let mut local = FlowLocal::new();
2800        let mut view = ContextView::new(&mut world, &mut local);
2801
2802        let taken = view.take_global(0);
2803        assert_eq!(taken, array, "read-through gives the World's current value");
2804        assert_eq!(
2805            view.global(0),
2806            &Value::Null,
2807            "the flow's own override layer must now read Null"
2808        );
2809        assert_eq!(
2810            world.global(0),
2811            &array,
2812            "World's own copy is untouched — Local writes never land in World"
2813        );
2814    }
2815}
2816
2817/// [`FrameStartView`] (issue #937, `docs/effects-spec.md` §12.2 "borrow,
2818/// don't copy"): the borrowing replacement for batch mode's per-flow
2819/// `frame_start.clone()`.
2820///
2821/// The load-bearing property is **observational equivalence with the clone
2822/// it replaces** — `frame_start ⊕ own writes`, cell for cell — so the
2823/// centerpiece here is `equivalent_to_stepping_against_a_private_clone`,
2824/// which replays one op script against both a real clone and a view and
2825/// compares every readable cell after every op. The rest pin the individual
2826/// mechanics that equivalence rests on.
2827#[cfg(test)]
2828mod frame_start_view_tests {
2829    use brink_format::DefinitionTag;
2830
2831    use super::*;
2832    use crate::rng::FastRng;
2833
2834    fn knot(n: u64) -> DefinitionId {
2835        DefinitionId::new(DefinitionTag::Address, n)
2836    }
2837
2838    /// A frame-start world with some pre-existing state in every unit the
2839    /// view overlays, so a passthrough read is distinguishable from a
2840    /// default-initialized one.
2841    fn frame_start() -> World {
2842        let mut world = World::from_globals(
2843            vec![Value::Int(10), Value::Int(20), Value::Int(30)],
2844            ResolvedPolicy::all_world(),
2845        );
2846        world.visit_counts.insert(knot(1), 5);
2847        world.turn_counts.insert(knot(1), 7);
2848        world.turn_index = 42;
2849        world.rng_seed = 99;
2850        world.previous_random = 77;
2851        world
2852    }
2853
2854    /// Every readable cell of a `ContextAccess`, as one comparable value:
2855    /// `(globals, visit counts, turn counts, turn index, rng seed, previous
2856    /// random)`. This is the observation vector the equivalence test diffs —
2857    /// if two contexts compare equal on it, nothing the VM can ask either one
2858    /// tells them apart.
2859    type Observation = (Vec<Value>, Vec<u32>, Vec<Option<u32>>, u32, i32, i32);
2860
2861    /// Read every cell of `ctx` into one [`Observation`].
2862    fn snapshot(ctx: &impl ContextAccess) -> Observation {
2863        (
2864            (0..3).map(|i| ctx.global(i).clone()).collect(),
2865            (0..3).map(|i| ctx.visit_count(knot(i))).collect(),
2866            (0..3).map(|i| ctx.turn_count(knot(i))).collect(),
2867            ctx.turn_index(),
2868            ctx.rng_seed(),
2869            ctx.previous_random(),
2870        )
2871    }
2872
2873    /// One mutation, applied identically to both sides of the equivalence
2874    /// comparison.
2875    #[derive(Clone, Copy)]
2876    enum Op {
2877        SetGlobal(u32, i32),
2878        TakeGlobal(u32),
2879        IncrementVisit(u64),
2880        SetVisitCount(u64, u32),
2881        SetTurnCount(u64, u32),
2882        IncrementTurnIndex,
2883        SetTurnIndex(u32),
2884        SetRngSeed(i32),
2885        SetPreviousRandom(i32),
2886    }
2887
2888    /// Apply `op`, returning anything it hands back (only `TakeGlobal` does)
2889    /// so the two sides' return values can be compared too, not just the
2890    /// resulting state.
2891    fn apply(ctx: &mut impl ContextAccess, op: Op) -> Option<Value> {
2892        match op {
2893            Op::SetGlobal(idx, v) => {
2894                ctx.set_global(idx, Value::Int(v));
2895                None
2896            }
2897            Op::TakeGlobal(idx) => Some(ctx.take_global(idx)),
2898            Op::IncrementVisit(id) => {
2899                ctx.increment_visit(knot(id));
2900                None
2901            }
2902            Op::SetVisitCount(id, c) => {
2903                ctx.set_visit_count(knot(id), c);
2904                None
2905            }
2906            Op::SetTurnCount(id, t) => {
2907                ctx.set_turn_count(knot(id), t);
2908                None
2909            }
2910            Op::IncrementTurnIndex => {
2911                ctx.increment_turn_index();
2912                None
2913            }
2914            Op::SetTurnIndex(i) => {
2915                ctx.set_turn_index(i);
2916                None
2917            }
2918            Op::SetRngSeed(s) => {
2919                ctx.set_rng_seed(s);
2920                None
2921            }
2922            Op::SetPreviousRandom(v) => {
2923                ctx.set_previous_random(v);
2924                None
2925            }
2926        }
2927    }
2928
2929    /// **The property this type exists to preserve.** Replay one op script
2930    /// against (a) a private `World` clone — the mechanism `FrameStartView`
2931    /// replaces — and (b) a view borrowing the same frame-start world, and
2932    /// assert the two are indistinguishable through `ContextAccess` after
2933    /// every single op, including each op's own return value. Covers all
2934    /// nine mutating entry points, exercising each cell both before and
2935    /// after it enters the overlay (the two branches every read has).
2936    #[test]
2937    fn equivalent_to_stepping_against_a_private_clone() {
2938        let script = [
2939            // Reads before any write: pure passthrough on the view side.
2940            Op::IncrementVisit(1), // CoW increment off a non-zero base
2941            Op::IncrementVisit(1), // ...then off the overlay's own value
2942            Op::IncrementVisit(2), // ...and off an absent (zero) base
2943            Op::SetVisitCount(0, 3),
2944            Op::SetTurnCount(1, 11), // overwrite a present turn count
2945            Op::SetTurnCount(2, 13), // set an absent one
2946            Op::IncrementTurnIndex,
2947            Op::IncrementTurnIndex,
2948            Op::SetTurnIndex(100),
2949            Op::SetRngSeed(-5), // first RNG write must capture both halves
2950            Op::SetPreviousRandom(6),
2951            Op::SetGlobal(0, 111),
2952            Op::TakeGlobal(0), // take an already-overlaid slot (a real move)
2953            Op::TakeGlobal(1), // take a slot still in the frame-start world
2954            Op::TakeGlobal(1), // ...and again, now that it is overlaid
2955            Op::SetGlobal(1, 222),
2956            Op::SetGlobal(2, 333),
2957        ];
2958
2959        let world = frame_start();
2960        let mut cloned = world.clone();
2961        let mut view = FrameStartView::new(&world);
2962
2963        assert_eq!(
2964            snapshot(&cloned),
2965            snapshot(&view),
2966            "a fresh view must read identically to a fresh clone"
2967        );
2968
2969        for (step, op) in script.into_iter().enumerate() {
2970            let from_clone = apply(&mut cloned, op);
2971            let from_view = apply(&mut view, op);
2972            assert_eq!(from_clone, from_view, "op {step} returned differently");
2973            assert_eq!(
2974                snapshot(&cloned),
2975                snapshot(&view),
2976                "diverged after op {step}"
2977            );
2978        }
2979
2980        // The whole point: none of that reached the borrowed world.
2981        assert_eq!(snapshot(&world), snapshot(&frame_start()));
2982    }
2983
2984    /// The concurrency property the parallel batch driver depends on: peer
2985    /// views over one shared `&World` are mutually invisible, so the order
2986    /// they are stepped in cannot affect any of their outcomes.
2987    #[test]
2988    fn peer_views_over_one_world_are_independent() {
2989        let world = frame_start();
2990        let mut a = FrameStartView::new(&world);
2991        let mut b = FrameStartView::new(&world);
2992
2993        a.set_global(0, Value::Int(1));
2994        a.increment_visit(knot(1));
2995        a.set_turn_index(1);
2996        a.set_rng_seed(1);
2997
2998        assert_eq!(b.global(0), &Value::Int(10), "peer write must be invisible");
2999        assert_eq!(b.visit_count(knot(1)), 5);
3000        assert_eq!(b.turn_index(), 42);
3001        assert_eq!(b.rng_seed(), 99);
3002
3003        b.set_global(0, Value::Int(2));
3004        assert_eq!(a.global(0), &Value::Int(1), "a still reads its own write");
3005    }
3006
3007    /// `take_global` keeps value-model-spec §5's move discipline: the first
3008    /// take of a slot clones once out of the shared frame-start world (it may
3009    /// not move out of a borrow), and every take after that is a real
3010    /// `mem::replace` move of the overlay's own allocation.
3011    #[test]
3012    fn take_global_clones_once_then_moves() {
3013        let array = Value::array(vec![Value::Int(1), Value::Int(2)]);
3014        let external = Arc::clone(array.as_array().expect("array"));
3015        let world = World::from_globals(vec![array], ResolvedPolicy::all_world());
3016        assert_eq!(Arc::strong_count(&external), 2, "world's slot + external");
3017
3018        let mut view = FrameStartView::new(&world);
3019
3020        // First take: one clone off the shared world — the refcount rises,
3021        // and the world keeps its own copy.
3022        let first = view.take_global(0);
3023        assert_eq!(
3024            Arc::strong_count(&external),
3025            3,
3026            "world's slot + external + the clone this take made"
3027        );
3028        assert_eq!(view.global(0), &Value::Null);
3029        assert_eq!(
3030            Arc::as_ptr(world.global(0).as_array().expect("array")),
3031            Arc::as_ptr(&external),
3032            "the borrowed world still holds its own value"
3033        );
3034
3035        // Put it back and take again: now the slot is the overlay's own, so
3036        // the take is a move — same allocation out, no refcount bump.
3037        view.set_global(0, first);
3038        let before = Arc::strong_count(&external);
3039        let second = view.take_global(0);
3040        assert_eq!(
3041            Arc::as_ptr(second.as_array().expect("array")),
3042            Arc::as_ptr(&external),
3043            "take must return the SAME allocation, not a copy"
3044        );
3045        assert_eq!(
3046            Arc::strong_count(&external),
3047            before,
3048            "the take itself must not bump the refcount"
3049        );
3050        assert_eq!(view.global(0), &Value::Null);
3051    }
3052
3053    /// `next_random`/`random_sequence` are pure functions of the seed they
3054    /// are handed, so the view must answer exactly as the borrowed world
3055    /// does — including after the view has overlaid its own RNG stream.
3056    #[test]
3057    fn random_helpers_delegate_to_the_borrowed_world() {
3058        let world = frame_start();
3059        let mut view = FrameStartView::new(&world);
3060
3061        assert_eq!(
3062            view.next_random::<FastRng>(3),
3063            world.next_random::<FastRng>(3)
3064        );
3065        assert_eq!(
3066            view.random_sequence::<FastRng>(3, 4),
3067            world.random_sequence::<FastRng>(3, 4)
3068        );
3069
3070        view.set_rng_seed(1234);
3071        assert_eq!(
3072            view.next_random::<FastRng>(3),
3073            world.next_random::<FastRng>(3),
3074            "the overlaid stream must not change how an explicit seed draws"
3075        );
3076    }
3077}