brink_format/value.rs
1use alloc::string::String;
2use alloc::sync::Arc;
3use alloc::vec;
4use alloc::vec::Vec;
5
6use serde::{Deserialize, Serialize};
7
8use crate::id::{DefinitionId, NameId};
9
10/// Maximum nesting depth permitted when decoding `VAL_ARRAY`/`VAL_MAP`
11/// values. Generous for legitimate data but bounds worst-case recursion so a
12/// crafted file of nested single-element arrays (~5 bytes/level) cannot
13/// stack-overflow the reader (CLAUDE.md "guard against unbounded growth";
14/// issue #553).
15///
16/// This is the single canonical definition, shared by every `decode_value`
17/// implementation that recurses on collection values — the `.inkb` reader
18/// (`brink_format::inkb::read`) and the runtime transcript reader
19/// (`brink_runtime::transcript`) both reference this constant rather than
20/// each defining their own copy (issue #561).
21pub const MAX_DECODE_DEPTH: usize = 128;
22
23/// Identifies a struct shape (TM-4, `docs/typed-mode-spec.md` §6) within the
24/// compiled story's `StructShapes` section (`docs/format-spec.md`, section
25/// tag `0x0C`).
26///
27/// Distinct from [`crate::id::DefinitionId`]: a `STRUCT` declaration is a
28/// compile-time nominal type, not a runtime storage location, so its wire
29/// footprint is this flat `u32` index into `StructShapes`, not a tagged
30/// definition id. Assigned deterministically at codegen time (sorted by
31/// declared struct name, never `HashMap` iteration order — CLAUDE.md).
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
33pub struct ShapeId(pub u32);
34
35/// The runtime type of a [`Value`].
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
37pub enum ValueType {
38 Int,
39 Float,
40 Bool,
41 String,
42 List,
43 DivertTarget,
44 VariablePointer,
45 TempPointer,
46 Null,
47 FragmentRef,
48 /// A copy-on-write ordered collection ([`Value::Array`]).
49 Array,
50 /// A copy-on-write insertion-ordered map ([`Value::Map`]).
51 Map,
52 /// A closed-shape, copy-on-write record ([`Value::Record`]) — TM-4.
53 Record,
54 /// A zero-bound function value ([`Value::FnRef`]) — T1c.
55 FnRef,
56 /// A function value with a bound-arg prefix ([`Value::Closure`]) — T1c.
57 Closure,
58 /// An opaque host-resource token ([`Value::Handle`]) — T1d.
59 Handle,
60 /// A symbolic path projection ([`Value::Projection`]) — T1e.
61 Projection,
62 /// A typed-absence value ([`Value::OptionVal`]) — NS-A1, `Option[T]`.
63 Option,
64 /// An integer range value ([`Value::Range`]) — NS-A5, F7.
65 Range,
66 /// A 2-lane f32 vector ([`Value::Vec2`]) — NS-A8, the numeric tower.
67 Vec2,
68 /// A 3-lane f32 vector ([`Value::Vec3`]) — NS-A8.
69 Vec3,
70 /// A 4-lane f32 vector ([`Value::Vec4`]) — NS-A8.
71 Vec4,
72 /// A rotation quaternion, `(x, y, z, w)` ([`Value::Quat`]) — NS-A8.
73 Quat,
74 /// A column-major 2×2 f32 matrix ([`Value::Mat2`]) — NS-A8.
75 Mat2,
76 /// A column-major 3×3 f32 matrix ([`Value::Mat3`]) — NS-A8.
77 Mat3,
78 /// A column-major 4×4 f32 matrix ([`Value::Mat4`]) — NS-A8.
79 Mat4,
80 /// A weighted table ([`Value::Weighted`]) — NS-A7, `Weighted[T]`.
81 Weighted,
82}
83
84/// A runtime value in the ink VM.
85///
86/// Heap-allocating variants (`String`, `List`, `Array`, `Map`, `Record`,
87/// `Closure`, `Projection`, `OptionVal`, `Weighted`) are wrapped in `Arc` so
88/// that cloning a `Value` is always O(1) — a refcount bump, not a deep copy —
89/// which makes call-frame cloning (during `fork_thread`) essentially free.
90/// Atomic refcounts are used so `Value` can flow through Bevy's parallel
91/// scheduler.
92///
93/// This `Arc` wrapping is a *performance* mechanism under **value
94/// semantics**, not reference semantics: sharing the underlying allocation
95/// is unobservable (`docs/value-model-spec.md` §3), because every mutating
96/// entry point forks the shared allocation on write (take → `make_mut` →
97/// write-back — see `docs/runtime-spec.md`'s "Value model" section). A
98/// binding that never observed a mutation never sees it, regardless of
99/// whether it shares the `Arc` underneath.
100///
101/// The `Array`/`Map` collections follow the ratified value model
102/// (`docs/value-model-spec.md` §4/§5): value semantics with copy-on-write
103/// sharing. Sharing is unobservable (§3) — [equality](Value#impl-PartialEq-for-Value)
104/// is structural with an `Arc::ptr_eq` fast path, and mutation goes through
105/// the take → [`make_mut`](Value::array_make_mut) → write-back RMW discipline
106/// so an unshared collection mutates in place and a shared one performs a
107/// single copy.
108///
109/// `PartialEq` is implemented by hand rather than derived so the collection
110/// arms can take the `ptr_eq` shortcut; every scalar arm matches what the
111/// derive would have produced.
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub enum Value {
114 Int(i32),
115 Float(f32),
116 Bool(bool),
117 String(Arc<str>),
118 List(Arc<ListValue>),
119 DivertTarget(DefinitionId),
120 /// A reference to a global variable, used for `ref` parameters.
121 VariablePointer(DefinitionId),
122 /// A runtime-only pointer to a temp in a specific call frame.
123 /// Used for `ref` parameters that target temp variables.
124 TempPointer {
125 slot: u16,
126 frame_depth: u16,
127 },
128 Null,
129 /// A reference to a fragment in the output buffer's fragment store.
130 /// Fragments preserve structural output parts for locale re-rendering.
131 FragmentRef(u32),
132 /// An ordered, copy-on-write collection of values (value-model-spec §4).
133 ///
134 /// The backing `Vec` is shared behind an `Arc`; clone is a refcount bump.
135 /// Mutation goes through [`array_make_mut`](Self::array_make_mut), which
136 /// copies the backing vector only when the `Arc` is shared.
137 Array(Arc<Vec<Value>>),
138 /// An insertion-ordered, copy-on-write map with scalar keys
139 /// (value-model-spec §4). Keys are `int`/`string`/`bool` ([`MapKey`]);
140 /// iteration order is insertion order, so the value is deterministic
141 /// without sorting or hashing.
142 Map(Arc<OrderedMap>),
143 /// A closed-shape record (typed-dialect era, TM-4;
144 /// `docs/value-model-spec.md` §11c / `docs/typed-mode-spec.md` §6).
145 ///
146 /// `fields` is a flat, shape-ordered vector shared behind an `Arc` — the
147 /// exact same COW discipline as [`Array`](Self::Array)/[`Map`](Self::Map):
148 /// clone is a refcount bump, mutation goes through
149 /// [`record_make_mut`](Self::record_make_mut). The shape is closed (no
150 /// dynamic add/remove of fields) and identified by [`ShapeId`], an
151 /// interned index into the compiled story's `StructShapes` table — two
152 /// records are never equal unless their shapes match, even if their
153 /// field vectors happen to coincide.
154 Record {
155 shape: ShapeId,
156 fields: Arc<Vec<Value>>,
157 },
158 /// A **zero-bound function value** (`docs/t1c-spec.md` §1/§6, wire tag
159 /// `VAL_FN_REF`). Partial application over a statically-named function
160 /// with no bound-arg prefix — `#fn(name)` where the target has no `ref`
161 /// params. The `DefinitionId` is the fn token (hashes from the target's
162 /// name, so a saved token survives recompiles that only edit the body).
163 ///
164 /// A no-payload scalar (`Copy`-cheap `DefinitionId`); no `Arc` needed.
165 /// Structural equality is "same fn token" (§5, sharing-unobservable).
166 FnRef(DefinitionId),
167 /// A **function value with a bound-arg prefix** (`docs/t1c-spec.md`
168 /// §1/§6, wire tag `VAL_CLOSURE`). Despite the name there is no lexical
169 /// environment — ink has no free variables; the "env" is the bound
170 /// prefix of the target's declared params (a `val` entry snapshots the
171 /// value at creation, a `ref` entry captures a durable cell as a
172 /// [`VariablePointer`](Self::VariablePointer)).
173 ///
174 /// Wrapped in `Arc` for the same O(1)-clone discipline as the collection
175 /// variants. Structural equality (§5): same fn token **and** equal env
176 /// rows — `ref` entries compare by bound cell, `val` entries by value,
177 /// both of which fall out of the derived per-entry comparison because a
178 /// `ref` payload is a `VariablePointer` (compared by its `DefinitionId`).
179 Closure(Arc<ClosureValue>),
180 /// An opaque host-resource token (`docs/t1d-spec.md` §1/§2, wire tag
181 /// `VAL_HANDLE`). Host resources (entities, audio instances, assets,
182 /// timers) enter the script world as `Handle` tokens: `{kind, id}`
183 /// scalars with value semantics — copied like ints, serializable,
184 /// compared by token. No live pointer ever lives in a `Value`;
185 /// dereferencing happens only host-side, against the host's registry,
186 /// inside bindings (the sharing-unobservable dogma applies verbatim — a
187 /// handle is a *name*, re-bound at a defined seam).
188 ///
189 /// `kind` is a [`NameId`] into the compiled story's manifest-declared
190 /// kind vocabulary (analyzer-side, not the format — the format ships
191 /// only the token shape, `docs/format-v4-rfc.md` §2). `id` is an opaque
192 /// `u64` the host allocates; the script never interprets it.
193 ///
194 /// A no-payload scalar (`Copy`-cheap: `NameId` + `u64`); no `Arc`
195 /// needed. Structural equality is token equality (`kind == kind && id
196 /// == id`, §6) — no ordering exists (any `<`/`>`/`<=`/`>=` is a runtime
197 /// fault in gradual mode, a compile error under the typed dialect), and
198 /// a handle is never a legal map key (the domain stays
199 /// int/string/bool — same restriction as function values).
200 ///
201 /// **No literal syntax and no dedicated opcode construct this value —
202 /// handles enter the script world only via bindings** (`docs/t1d-spec.md`
203 /// §2 RULED). This variant, its wire encoding, and its `.inkt` atom
204 /// exist so a handle received from a binding can flow through globals,
205 /// collections, saves, and the transcript like any other value.
206 Handle {
207 /// The manifest-declared kind name (analyzer-side vocabulary).
208 kind: NameId,
209 /// The host-allocated token id. Opaque to the script.
210 id: u64,
211 },
212 /// A symbolic path projection (`docs/t1e-spec.md` §1/§3, wire tag
213 /// `VAL_PROJECTION`): `(root cell, path segments)` — never an interior
214 /// pointer. Created only in `ref`-argument position (`ref
215 /// npc.inventory[3]`); reads walk the path against the root's *current*
216 /// value, writes desugar to root-cell RMW (take → walk → `make_mut`
217 /// spine → write → store back, spec §3). The segment list is fixed at
218 /// creation ("index expressions snapshot at `ref` creation", spec §1) —
219 /// there is no lazy re-evaluation.
220 ///
221 /// Wrapped in `Arc` for the same O(1)-clone discipline as
222 /// [`Closure`](Self::Closure). Structural equality (spec §4 PROPOSED,
223 /// implemented here): same root cell and equal segments.
224 Projection(Arc<ProjectionValue>),
225 /// A typed-absence value — the compiler-owned `Option[T]` enum (NS-A1,
226 /// `docs/stdlib-spec.md` §1.1/§1.4, ruled 2026-07-18: "a fault says
227 /// 'your program is wrong'; Option says 'the world didn't have one'").
228 ///
229 /// `Option[T]` is the third compiler-known parameterized builtin
230 /// (joining `[T]`/`[K: V]`) — a compiler-owned enum shape, NOT user
231 /// generics. Runtime representation: `None` is payload-free (no
232 /// allocation); `Some` wraps its inner value behind an `Arc` so clone
233 /// stays O(1) like every other heap-bearing variant. Nesting is legal
234 /// and meaningful (`some(none) != none` — the wire and equality both
235 /// preserve it).
236 ///
237 /// Named `OptionVal` (not `Option`) purely to avoid the eternal
238 /// `core::option::Option` shadowing hazard inside `match` arms; the
239 /// [`ValueType`] discriminant and every author-facing surface still
240 /// say "option".
241 ///
242 /// Structural equality: `none == none`; `some(x) == some(y)` iff
243 /// `x == y`; an Option is never equal to a bare `T` (the ruled
244 /// `Option[T] ≠ T` strictness holds at the value layer too). Display
245 /// (`stringify`/`string(x)`): `none` / `some(<inner>)` — the boring,
246 /// stable form, total forever (F28). The §1.6b display-boundary
247 /// forgiveness (Track B4) is a `brink-runtime`-only concern layered on
248 /// top at read time (`value_ops::stringify_display`) — deliberately
249 /// not implemented at this value-definition layer, since `string()`
250 /// and every non-display consumer must keep seeing the total form.
251 OptionVal(Option<Arc<Value>>),
252 /// An integer range value (NS-A5, `docs/stdlib-spec.md` §7 — F7, ruled
253 /// 2026-07-19: "ranges are a REAL Value kind"). `start..end` (exclusive)
254 /// or `start..=end` (inclusive) over `int` bounds — v1 is int-only.
255 ///
256 /// Ranges join the closed iterable set (`for i in 0..n`), index like a
257 /// virtual array of their elements, and are the substrate of the
258 /// language's first value refinement (the inhabited range consumed by
259 /// `rand::int`). A durable wire form exists (`VAL_RANGE`) because
260 /// `FlowFrame` spills for-loop iterators across `await` — a range held
261 /// in a loop snapshot must survive save/load.
262 ///
263 /// A no-payload scalar (two `i32`s + a flag); no `Arc` needed. The
264 /// **written form is preserved** — `1..7` and `1..=6` keep their
265 /// `inclusive` flag through saves, the transcript, and display — but
266 /// **equality is content equality** (F7's ruling word): two ranges are
267 /// equal iff they denote the same integer sequence, so `1..=6 == 1..7`
268 /// and every empty range equals every other empty range. This is the
269 /// same content-over-form posture as the #909 map-equality ruling
270 /// (insertion order iterates, content compares).
271 Range {
272 /// The first element of the range (always inclusive).
273 start: i32,
274 /// The written end bound; whether it is an element depends on
275 /// `inclusive`.
276 end: i32,
277 /// `true` for the `..=` form (`end` is the last element), `false`
278 /// for the `..` form (`end` is one past the last element).
279 inclusive: bool,
280 },
281 /// A 2-lane f32 vector (NS-A8, `docs/tower-mini-spec.md` T1: the tower
282 /// value kinds are **glam-backed** — glam is the in-memory compute type,
283 /// so vector/quaternion/matrix ops arrive correct-by-construction).
284 ///
285 /// Serde discipline (T5): the derive on `Value` routes every tower
286 /// variant through the hand-written lane modules in [`tower_serde`] —
287 /// explicit `x, y(, z, w)` lane order for vectors and the quat,
288 /// column-major column-by-column for matrices — NEVER glam's memory
289 /// representation (which varies with SIMD features and versions) and
290 /// never glam's own `serde` feature (kept off in `Cargo.toml`).
291 ///
292 /// Equality (T4): componentwise IEEE via glam's derived `PartialEq` — a
293 /// NaN-bearing vector never equals itself, `-0.0 == +0.0` per lane,
294 /// exactly like bare `Float`. Tower values are NOT orderable (§4b: a
295 /// vector in an ordering context is a `NotOrderable` fault) and are
296 /// never legal map keys (`MapKey::from_value` has no tower arms).
297 Vec2(#[serde(with = "tower_serde::vec2")] glam::Vec2),
298 /// A 3-lane f32 vector (NS-A8). The **unaligned** `glam::Vec3` (not
299 /// `Vec3A`) per the mini-spec — aligned variants would bloat every
300 /// `Value`. See [`Vec2`](Self::Vec2) for the shared tower discipline.
301 Vec3(#[serde(with = "tower_serde::vec3")] glam::Vec3),
302 /// A 4-lane f32 vector (NS-A8). See [`Vec2`](Self::Vec2).
303 Vec4(#[serde(with = "tower_serde::vec4")] glam::Vec4),
304 /// A rotation quaternion (NS-A8), lane order `(x, y, z, w)` per glam
305 /// (T3: conventions per glam, wholesale — right-handed, `quat * quat`
306 /// composes, `quat * vec` rotates). See [`Vec2`](Self::Vec2).
307 Quat(#[serde(with = "tower_serde::quat")] glam::Quat),
308 /// A column-major 2×2 f32 matrix (NS-A8, T2: all matrix sizes ship).
309 /// See [`Vec2`](Self::Vec2).
310 Mat2(#[serde(with = "tower_serde::mat2")] glam::Mat2),
311 /// A column-major 3×3 f32 matrix (NS-A8). The **unaligned** `glam::Mat3`
312 /// (not `Mat3A`). See [`Vec2`](Self::Vec2).
313 Mat3(#[serde(with = "tower_serde::mat3")] glam::Mat3),
314 /// A column-major 4×4 f32 matrix (NS-A8). See [`Vec2`](Self::Vec2).
315 Mat4(#[serde(with = "tower_serde::mat4")] glam::Mat4),
316 /// A weighted table (NS-A7, `docs/stdlib-spec.md` §8): `Weighted[T]` —
317 /// positive-int weights over values, in construction order.
318 /// **Evidence-by-construction**: the only producer (`weighted_new`)
319 /// refuses empty tables and non-positive/non-int weights, so a
320 /// `Weighted` that exists is always a valid `roll` target (total). The
321 /// entry row is a **multiset** (F17: duplicate weights legal and
322 /// meaningful — deliberately divergent from `Map`'s key-set). v1 is
323 /// construct-and-roll: no `len`, no iteration, no mutation.
324 Weighted(Arc<WeightedValue>),
325}
326
327/// Hand-written serde lane codecs for the tower variants (NS-A8,
328/// `docs/tower-mini-spec.md` T5): each type serializes as its flat lane
329/// array — vectors and the quat as `[x, y(, z, w)]`, matrices as their
330/// column-major `to_cols_array()` — and deserializes back through glam's
331/// explicit `from_array`/`from_cols_array` constructors. Glam computes; the
332/// serialized form is ours: no glam memory layout, no serde-through-glam.
333pub mod tower_serde {
334 /// Expand one lane codec module: `to`/`from` are the explicit
335 /// lane-array conversions (never a memory-layout cast). Matrix `from`
336 /// constructors (`from_cols_array`) take the array by reference, hence
337 /// the closure rather than a bare path.
338 macro_rules! lane_codec {
339 ($name:ident, $ty:ty, $lanes:literal, $to:ident, |$a:ident| $from:expr) => {
340 pub mod $name {
341 use serde::{Deserialize, Deserializer, Serialize, Serializer};
342
343 pub fn serialize<S: Serializer>(v: &$ty, s: S) -> Result<S::Ok, S::Error> {
344 v.$to().serialize(s)
345 }
346
347 pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<$ty, D::Error> {
348 <[f32; $lanes]>::deserialize(d).map(|$a| $from)
349 }
350 }
351 };
352 }
353
354 lane_codec!(vec2, glam::Vec2, 2, to_array, |a| glam::Vec2::from_array(a));
355 lane_codec!(vec3, glam::Vec3, 3, to_array, |a| glam::Vec3::from_array(a));
356 lane_codec!(vec4, glam::Vec4, 4, to_array, |a| glam::Vec4::from_array(a));
357 lane_codec!(quat, glam::Quat, 4, to_array, |a| glam::Quat::from_array(a));
358 lane_codec!(mat2, glam::Mat2, 4, to_cols_array, |a| {
359 glam::Mat2::from_cols_array(&a)
360 });
361 lane_codec!(mat3, glam::Mat3, 9, to_cols_array, |a| {
362 glam::Mat3::from_cols_array(&a)
363 });
364 lane_codec!(mat4, glam::Mat4, 16, to_cols_array, |a| {
365 glam::Mat4::from_cols_array(&a)
366 });
367}
368
369/// The payload of a [`Value::Projection`] — the root cell plus its ordered
370/// segment chain (`docs/t1e-spec.md` §1/§3).
371#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
372pub struct ProjectionValue {
373 /// The durable root cell this projection reads/writes through (a global
374 /// `VAR`/`#@local` — never a temp, enforced at compile time by T1e-1's
375 /// E080 durable-root check). Mirrors the `VAL_VAR_POINTER` payload shape
376 /// (`docs/format-v4-rfc.md` §1: "cell reference … reused not
377 /// reinvented").
378 pub cell: DefinitionId,
379 /// The ordered path segments, fixed at creation.
380 pub segments: Vec<ProjSegment>,
381}
382
383/// One path-projection segment (`docs/format-v4-rfc.md` §1: `segments: 0 =
384/// index i32, 1 = key value`). Segment kind `2 = range` is RESERVED and never
385/// constructed in T1e (icebox #829 — sequence slices/ranges).
386///
387/// The kind recorded here is a **wire-compactness choice**, not a semantic
388/// tag the walker trusts blindly: an evaluated segment value that happens to
389/// be an `Int` is captured as [`Index`](Self::Index) (the compact i32 form);
390/// everything else — a map key of another scalar type, or a struct field
391/// name (always a `Value::String` literal) — is captured as
392/// [`Key`](Self::Key). Walking dispatches on the *root's current container
393/// type* at each step (spec §4: reads walk against the root's current
394/// value), so an `Index(n)` segment applied to a `Map` is reinterpreted as
395/// `MapKey::Int(n)` — the distinction never forecloses either domain.
396#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
397pub enum ProjSegment {
398 /// `[i]` — captured because the evaluated segment value was an `Int`.
399 Index(i32),
400 /// `[k]` (a non-`Int` map key) or `.field` (a struct field name,
401 /// captured as `Value::String`).
402 Key(Value),
403}
404
405impl ProjSegment {
406 /// Build a segment from an evaluated `Value` — the classification rule
407 /// this whole module documents: `Int` → [`Index`](Self::Index), anything
408 /// else → [`Key`](Self::Key).
409 #[must_use]
410 pub fn from_value(v: Value) -> Self {
411 match v {
412 Value::Int(n) => Self::Index(n),
413 other => Self::Key(other),
414 }
415 }
416}
417
418/// The payload of a [`Value::Closure`] — the fn token plus its bound-arg
419/// prefix (`docs/t1c-spec.md` §1/§6).
420#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
421pub struct ClosureValue {
422 /// The target function's [`DefinitionId`] (the fn token).
423 pub target: DefinitionId,
424 /// The bound-arg prefix, in the target's declared param order. The
425 /// entries carry the param **name and mode** deliberately (spec §6): on
426 /// load/invoke after a recompile they are validated against the current
427 /// signature so a renamed/re-moded param faults cleanly instead of
428 /// silently misbinding.
429 pub env: Vec<ClosureEnvEntry>,
430}
431
432/// One bound-arg entry in a [`ClosureValue`]'s env.
433#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
434pub struct ClosureEnvEntry {
435 /// The bound param's interned name (redundancy for rehydration checking).
436 pub name: NameId,
437 /// `true` if the target declared this param `ref` (the payload is a
438 /// captured cell), `false` for a `val` snapshot.
439 pub is_ref: bool,
440 /// The bound value: a snapshot (`val`) or a captured cell reference
441 /// (`ref` — a [`VariablePointer`](Value::VariablePointer)).
442 pub payload: Value,
443}
444
445impl Value {
446 /// Return the type discriminant for this value.
447 pub fn value_type(&self) -> ValueType {
448 match self {
449 Self::Int(_) => ValueType::Int,
450 Self::Float(_) => ValueType::Float,
451 Self::Bool(_) => ValueType::Bool,
452 Self::String(_) => ValueType::String,
453 Self::List(_) => ValueType::List,
454 Self::DivertTarget(_) => ValueType::DivertTarget,
455 Self::VariablePointer(_) => ValueType::VariablePointer,
456 Self::TempPointer { .. } => ValueType::TempPointer,
457 Self::Null => ValueType::Null,
458 Self::FragmentRef(_) => ValueType::FragmentRef,
459 Self::Array(_) => ValueType::Array,
460 Self::Map(_) => ValueType::Map,
461 Self::Record { .. } => ValueType::Record,
462 Self::FnRef(_) => ValueType::FnRef,
463 Self::Closure(_) => ValueType::Closure,
464 Self::Handle { .. } => ValueType::Handle,
465 Self::Projection(_) => ValueType::Projection,
466 Self::OptionVal(_) => ValueType::Option,
467 Self::Range { .. } => ValueType::Range,
468 Self::Vec2(_) => ValueType::Vec2,
469 Self::Vec3(_) => ValueType::Vec3,
470 Self::Vec4(_) => ValueType::Vec4,
471 Self::Quat(_) => ValueType::Quat,
472 Self::Mat2(_) => ValueType::Mat2,
473 Self::Mat3(_) => ValueType::Mat3,
474 Self::Mat4(_) => ValueType::Mat4,
475 Self::Weighted(_) => ValueType::Weighted,
476 }
477 }
478
479 /// Build a [`Range`](Self::Range) from its written bounds.
480 pub fn range(start: i32, end: i32, inclusive: bool) -> Self {
481 Self::Range {
482 start,
483 end,
484 inclusive,
485 }
486 }
487
488 /// Borrow the `(start, end, inclusive)` triple if this value is a
489 /// [`Range`](Self::Range).
490 pub fn as_range(&self) -> Option<(i32, i32, bool)> {
491 match self {
492 Self::Range {
493 start,
494 end,
495 inclusive,
496 } => Some((*start, *end, *inclusive)),
497 _ => None,
498 }
499 }
500
501 /// The one-past-the-last element bound of a [`Range`](Self::Range),
502 /// normalized over the written form (`i64` so `1..=i32::MAX` cannot
503 /// overflow). `None` for any non-range value.
504 pub fn range_end_exclusive(&self) -> Option<i64> {
505 match self {
506 Self::Range { end, inclusive, .. } => {
507 Some(i64::from(*end) + i64::from(u8::from(*inclusive)))
508 }
509 _ => None,
510 }
511 }
512
513 /// Number of elements a [`Range`](Self::Range) denotes (`0` for an empty
514 /// range — a range never has negative length). `None` for any non-range
515 /// value. `i64` because `i32::MIN..=i32::MAX` has 2³² elements.
516 pub fn range_len(&self) -> Option<i64> {
517 match self {
518 Self::Range { start, .. } => {
519 let end_ex = self.range_end_exclusive()?;
520 Some((end_ex - i64::from(*start)).max(0))
521 }
522 _ => None,
523 }
524 }
525
526 /// Build a [`Weighted`](Self::Weighted) table from `(weight, value)`
527 /// entries. The caller owns the §8 evidence-by-construction invariant
528 /// (non-empty, positive weights) — the VM's `weighted_new` op and the
529 /// wire reader both validate before calling this.
530 #[must_use]
531 pub fn weighted(entries: Vec<(i32, Value)>) -> Self {
532 Self::Weighted(Arc::new(WeightedValue { entries }))
533 }
534
535 /// Build a `some(inner)` [`OptionVal`](Self::OptionVal).
536 pub fn some(inner: Value) -> Self {
537 Self::OptionVal(Some(Arc::new(inner)))
538 }
539
540 /// Build a `none` [`OptionVal`](Self::OptionVal).
541 pub fn none() -> Self {
542 Self::OptionVal(None)
543 }
544
545 /// Borrow the Option payload if this value is an
546 /// [`OptionVal`](Self::OptionVal): `Some(Some(&inner))` for `some(x)`,
547 /// `Some(None)` for `none`, `None` for any non-Option value.
548 pub fn as_option(&self) -> Option<Option<&Value>> {
549 match self {
550 Self::OptionVal(inner) => Some(inner.as_deref()),
551 _ => None,
552 }
553 }
554
555 /// Extract an `i32` if this value is an [`Int`](Self::Int).
556 ///
557 /// Strict: does not coerce floats or booleans. Returns `None` for any
558 /// other variant. For binding authors that want to read an integer
559 /// argument from ink.
560 pub fn as_int(&self) -> Option<i32> {
561 match self {
562 Self::Int(i) => Some(*i),
563 _ => None,
564 }
565 }
566
567 /// Extract an `f32` if this value is numeric.
568 ///
569 /// Lenient on the int → float direction only: an [`Int`](Self::Int) is
570 /// widened to `f32` (matching ink's implicit int→float promotion), but a
571 /// float is never truncated to an int by [`as_int`](Self::as_int).
572 pub fn as_float(&self) -> Option<f32> {
573 match self {
574 Self::Float(f) => Some(*f),
575 #[expect(
576 clippy::cast_precision_loss,
577 reason = "int->float promotion matches ink coercion semantics"
578 )]
579 Self::Int(i) => Some(*i as f32),
580 _ => None,
581 }
582 }
583
584 /// Extract a `bool` if this value is a [`Bool`](Self::Bool).
585 ///
586 /// Strict: does not treat nonzero numbers as truthy. Use the VM's own
587 /// truthiness rules if you need ink-style coercion.
588 pub fn as_bool(&self) -> Option<bool> {
589 match self {
590 Self::Bool(b) => Some(*b),
591 _ => None,
592 }
593 }
594
595 /// Borrow the string contents if this value is a [`String`](Self::String).
596 pub fn as_str(&self) -> Option<&str> {
597 match self {
598 Self::String(s) => Some(s),
599 _ => None,
600 }
601 }
602
603 /// Build an [`Array`](Self::Array) from a vector of values.
604 pub fn array(items: Vec<Value>) -> Self {
605 Self::Array(Arc::new(items))
606 }
607
608 /// Build a [`Map`](Self::Map) from an [`OrderedMap`].
609 pub fn map(map: OrderedMap) -> Self {
610 Self::Map(Arc::new(map))
611 }
612
613 /// Build a [`Record`](Self::Record) from a shape id and its field values
614 /// (already in the shape's declared field order — the caller, not this
615 /// constructor, is responsible for ordering).
616 pub fn record(shape: ShapeId, fields: Vec<Value>) -> Self {
617 Self::Record {
618 shape,
619 fields: Arc::new(fields),
620 }
621 }
622
623 /// Borrow the record payload if this value is a [`Record`](Self::Record).
624 pub fn as_record(&self) -> Option<(ShapeId, &Arc<Vec<Value>>)> {
625 match self {
626 Self::Record { shape, fields } => Some((*shape, fields)),
627 _ => None,
628 }
629 }
630
631 /// Build a [`Closure`](Self::Closure) from a target and its bound-arg
632 /// prefix (T1c, `docs/t1c-spec.md` §6).
633 pub fn closure(target: DefinitionId, env: Vec<ClosureEnvEntry>) -> Self {
634 Self::Closure(Arc::new(ClosureValue { target, env }))
635 }
636
637 /// The fn token (target [`DefinitionId`]) if this value is a function
638 /// value — [`FnRef`](Self::FnRef) or [`Closure`](Self::Closure).
639 pub fn fn_target(&self) -> Option<DefinitionId> {
640 match self {
641 Self::FnRef(target) => Some(*target),
642 Self::Closure(c) => Some(c.target),
643 _ => None,
644 }
645 }
646
647 /// Borrow the closure payload if this value is a [`Closure`](Self::Closure).
648 pub fn as_closure(&self) -> Option<&Arc<ClosureValue>> {
649 match self {
650 Self::Closure(c) => Some(c),
651 _ => None,
652 }
653 }
654
655 /// Borrow the weighted-table payload if this value is a
656 /// [`Weighted`](Self::Weighted).
657 pub fn as_weighted(&self) -> Option<&Arc<WeightedValue>> {
658 match self {
659 Self::Weighted(w) => Some(w),
660 _ => None,
661 }
662 }
663
664 /// Build a [`Handle`](Self::Handle) token from a kind and host-allocated
665 /// id (T1d, `docs/t1d-spec.md` §2). Note: this constructor is a plain
666 /// value builder, not a capability mint — the invariant that handles
667 /// only ever originate from a binding is enforced by the compiler (no
668 /// literal syntax, no opcode constructs this value), not by this type.
669 pub fn handle(kind: NameId, id: u64) -> Self {
670 Self::Handle { kind, id }
671 }
672
673 /// Borrow the `(kind, id)` pair if this value is a [`Handle`](Self::Handle).
674 pub fn as_handle(&self) -> Option<(NameId, u64)> {
675 match self {
676 Self::Handle { kind, id } => Some((*kind, *id)),
677 _ => None,
678 }
679 }
680
681 /// Build a [`Projection`](Self::Projection) from a root cell and its
682 /// ordered segment chain (`docs/t1e-spec.md` §1/§3).
683 pub fn projection(cell: DefinitionId, segments: Vec<ProjSegment>) -> Self {
684 Self::Projection(Arc::new(ProjectionValue { cell, segments }))
685 }
686
687 /// Borrow the projection payload if this value is a
688 /// [`Projection`](Self::Projection).
689 pub fn as_projection(&self) -> Option<&Arc<ProjectionValue>> {
690 match self {
691 Self::Projection(p) => Some(p),
692 _ => None,
693 }
694 }
695
696 /// Extract the glam payload if this value is a [`Vec2`](Self::Vec2) —
697 /// the NS-A8 identity-marshal read for binding authors (T1: glam is the
698 /// compute type on both sides of the boundary). Strict like
699 /// [`as_int`](Self::as_int): no cross-kind coercion.
700 pub fn as_vec2(&self) -> Option<glam::Vec2> {
701 match self {
702 Self::Vec2(v) => Some(*v),
703 _ => None,
704 }
705 }
706
707 /// Extract the glam payload if this value is a [`Vec3`](Self::Vec3).
708 pub fn as_vec3(&self) -> Option<glam::Vec3> {
709 match self {
710 Self::Vec3(v) => Some(*v),
711 _ => None,
712 }
713 }
714
715 /// Extract the glam payload if this value is a [`Vec4`](Self::Vec4).
716 pub fn as_vec4(&self) -> Option<glam::Vec4> {
717 match self {
718 Self::Vec4(v) => Some(*v),
719 _ => None,
720 }
721 }
722
723 /// Extract the glam payload if this value is a [`Quat`](Self::Quat).
724 pub fn as_quat(&self) -> Option<glam::Quat> {
725 match self {
726 Self::Quat(q) => Some(*q),
727 _ => None,
728 }
729 }
730
731 /// Extract the glam payload if this value is a [`Mat2`](Self::Mat2).
732 pub fn as_mat2(&self) -> Option<glam::Mat2> {
733 match self {
734 Self::Mat2(m) => Some(*m),
735 _ => None,
736 }
737 }
738
739 /// Extract the glam payload if this value is a [`Mat3`](Self::Mat3).
740 pub fn as_mat3(&self) -> Option<glam::Mat3> {
741 match self {
742 Self::Mat3(m) => Some(*m),
743 _ => None,
744 }
745 }
746
747 /// Extract the glam payload if this value is a [`Mat4`](Self::Mat4).
748 pub fn as_mat4(&self) -> Option<glam::Mat4> {
749 match self {
750 Self::Mat4(m) => Some(*m),
751 _ => None,
752 }
753 }
754
755 /// Borrow the array payload if this value is an [`Array`](Self::Array).
756 ///
757 /// Read-only: the returned slice never triggers a copy. Mutation uses
758 /// [`array_make_mut`](Self::array_make_mut).
759 pub fn as_array(&self) -> Option<&Arc<Vec<Value>>> {
760 match self {
761 Self::Array(items) => Some(items),
762 _ => None,
763 }
764 }
765
766 /// Borrow the map payload if this value is a [`Map`](Self::Map).
767 ///
768 /// Read-only: mutation uses [`map_make_mut`](Self::map_make_mut).
769 pub fn as_map(&self) -> Option<&Arc<OrderedMap>> {
770 match self {
771 Self::Map(map) => Some(map),
772 _ => None,
773 }
774 }
775
776 /// Copy-on-write mutable access to an [`Array`](Self::Array)'s backing
777 /// vector, or `None` for any other value.
778 ///
779 /// This is the `make_mut` step of the take → `make_mut` → write-back RMW
780 /// discipline (value-model-spec §5). When the backing `Arc` is unique the
781 /// mutation is in place (O(1) amortized append); when it is shared with a
782 /// snapshot or another slot, exactly one O(n) copy is made and the value
783 /// becomes unique again. Because sharing is unobservable (§3), callers
784 /// cannot tell which path was taken.
785 pub fn array_make_mut(&mut self) -> Option<&mut Vec<Value>> {
786 match self {
787 Self::Array(items) => Some(Arc::make_mut(items)),
788 _ => None,
789 }
790 }
791
792 /// Copy-on-write mutable access to a [`Map`](Self::Map)'s contents, or
793 /// `None` for any other value. See [`array_make_mut`](Self::array_make_mut)
794 /// for the RMW discipline this implements.
795 pub fn map_make_mut(&mut self) -> Option<&mut OrderedMap> {
796 match self {
797 Self::Map(map) => Some(Arc::make_mut(map)),
798 _ => None,
799 }
800 }
801
802 /// Copy-on-write mutable access to a [`Record`](Self::Record)'s flat
803 /// field vector, or `None` for any other value. See
804 /// [`array_make_mut`](Self::array_make_mut) for the RMW discipline this
805 /// implements — the shape itself never changes (closed shape), only
806 /// field values.
807 pub fn record_make_mut(&mut self) -> Option<&mut Vec<Value>> {
808 match self {
809 Self::Record { fields, .. } => Some(Arc::make_mut(fields)),
810 _ => None,
811 }
812 }
813}
814
815/// Structural equality with an `Arc::ptr_eq` fast path for collections
816/// (value-model-spec §4/§5).
817///
818/// Every scalar arm reproduces exactly what `#[derive(PartialEq)]` would emit.
819/// The `Array`/`Map` arms add the `ptr_eq` shortcut: two values that share the
820/// same `Arc` (the same snapshot) compare equal immediately, otherwise the
821/// comparison is element-wise structural. NaN-bearing collections that are
822/// *not* the same snapshot never compare equal, because `f32` equality
823/// composes structurally through the elements; a collection compared against
824/// *itself* (same `Arc`) is equal even if it contains a NaN — the spec calls
825/// this out as harmless and stated (§4).
826impl PartialEq for Value {
827 #[expect(
828 clippy::match_same_arms,
829 reason = "each scalar variant is spelled out so the mapping to the \
830 derive it replaces is auditable; merging identical `a == b` \
831 bodies would obscure which variants are covered"
832 )]
833 fn eq(&self, other: &Self) -> bool {
834 match (self, other) {
835 (Self::Int(a), Self::Int(b)) => a == b,
836 (Self::Float(a), Self::Float(b)) => a == b,
837 (Self::Bool(a), Self::Bool(b)) => a == b,
838 (Self::String(a), Self::String(b)) => a == b,
839 (Self::List(a), Self::List(b)) => a == b,
840 (Self::DivertTarget(a), Self::DivertTarget(b)) => a == b,
841 (Self::VariablePointer(a), Self::VariablePointer(b)) => a == b,
842 (
843 Self::TempPointer {
844 slot: a_slot,
845 frame_depth: a_depth,
846 },
847 Self::TempPointer {
848 slot: b_slot,
849 frame_depth: b_depth,
850 },
851 ) => a_slot == b_slot && a_depth == b_depth,
852 (Self::Null, Self::Null) => true,
853 (Self::FragmentRef(a), Self::FragmentRef(b)) => a == b,
854 (Self::Array(a), Self::Array(b)) => Arc::ptr_eq(a, b) || a == b,
855 (Self::Map(a), Self::Map(b)) => Arc::ptr_eq(a, b) || a == b,
856 (
857 Self::Record {
858 shape: sa,
859 fields: a,
860 },
861 Self::Record {
862 shape: sb,
863 fields: b,
864 },
865 ) => sa == sb && (Arc::ptr_eq(a, b) || a == b),
866 // Function values (T1c, docs/t1c-spec.md §5): structural equality
867 // is "same fn token and equal bound-arg rows". `FnRef` is the
868 // zero-bound case (same token). `Closure` adds the env comparison,
869 // with the `Arc::ptr_eq` fast path mirroring the collection arms;
870 // a `ref` env entry's `VariablePointer` payload compares by cell,
871 // a `val` entry by value — both fall out of `ClosureValue`'s
872 // derived `PartialEq`.
873 (Self::FnRef(a), Self::FnRef(b)) => a == b,
874 (Self::Closure(a), Self::Closure(b)) => Arc::ptr_eq(a, b) || a == b,
875 // Handle equality (T1d, docs/t1d-spec.md §6): token equality —
876 // same kind and same id, nothing else. No ordering exists (there
877 // is no `PartialOrd`/`Ord` impl for `Value`), and a handle is
878 // never a legal map key (`MapKey::from_value` has no `Handle`
879 // arm, so it falls through to `None` for this variant).
880 (Self::Handle { kind: ka, id: ida }, Self::Handle { kind: kb, id: idb }) => {
881 ka == kb && ida == idb
882 }
883 // Projection equality (T1e, docs/t1e-spec.md §4 PROPOSED): same
884 // root cell + equal segments, with the `Arc::ptr_eq` fast path
885 // mirroring every other heap-allocated variant.
886 (Self::Projection(a), Self::Projection(b)) => Arc::ptr_eq(a, b) || a == b,
887 // Option equality (NS-A1): structural — `none == none`,
888 // `some(x) == some(y)` iff `x == y`, with the `Arc::ptr_eq`
889 // fast path on the `some` payload mirroring every other
890 // heap-allocated variant. Cross-variant (`some(1) == 1`) falls
891 // through to `false` below — the ruled `Option[T] ≠ T`
892 // strictness at the value layer.
893 // Weighted equality (NS-A7): multiset content with the
894 // `Arc::ptr_eq` fast path — see `WeightedValue`'s `PartialEq`.
895 (Self::Weighted(a), Self::Weighted(b)) => Arc::ptr_eq(a, b) || a == b,
896 (Self::OptionVal(a), Self::OptionVal(b)) => match (a, b) {
897 (None, None) => true,
898 (Some(x), Some(y)) => Arc::ptr_eq(x, y) || x == y,
899 _ => false,
900 },
901 // Range equality (NS-A5, F7 "content equality"): two ranges are
902 // equal iff they denote the same integer sequence — the written
903 // form (`..` vs `..=`) is display fidelity, not content, so
904 // `1..=6 == 1..7`, and every empty range equals every other
905 // empty range (both denote the zero-length sequence, exactly as
906 // two empty arrays are equal). The #909 map ruling is the
907 // precedent: content compares, form displays.
908 (a @ Self::Range { start: sa, .. }, b @ Self::Range { start: sb, .. }) => {
909 let (la, lb) = (a.range_len(), b.range_len());
910 match (la, lb) {
911 (Some(0), Some(0)) => true,
912 (Some(x), Some(y)) => x == y && sa == sb,
913 // Unreachable: both sides are `Range`.
914 _ => false,
915 }
916 }
917 // Tower equality (NS-A8, `docs/tower-mini-spec.md` T4):
918 // componentwise IEEE via glam's own `PartialEq` — a NaN lane
919 // makes a value unequal to *itself*, `-0.0 == +0.0` per lane,
920 // exactly like the bare `Float` arm above. Cross-kind pairs
921 // (`Vec2` vs `Vec3`) fall through to `false` below, like every
922 // other cross-variant pair.
923 (Self::Vec2(a), Self::Vec2(b)) => a == b,
924 (Self::Vec3(a), Self::Vec3(b)) => a == b,
925 (Self::Vec4(a), Self::Vec4(b)) => a == b,
926 (Self::Quat(a), Self::Quat(b)) => a == b,
927 (Self::Mat2(a), Self::Mat2(b)) => a == b,
928 (Self::Mat3(a), Self::Mat3(b)) => a == b,
929 (Self::Mat4(a), Self::Mat4(b)) => a == b,
930 _ => false,
931 }
932 }
933}
934
935impl From<i32> for Value {
936 fn from(v: i32) -> Self {
937 Self::Int(v)
938 }
939}
940
941impl From<f32> for Value {
942 fn from(v: f32) -> Self {
943 Self::Float(v)
944 }
945}
946
947impl From<bool> for Value {
948 fn from(v: bool) -> Self {
949 Self::Bool(v)
950 }
951}
952
953impl From<&str> for Value {
954 fn from(v: &str) -> Self {
955 Self::String(Arc::from(v))
956 }
957}
958
959impl From<String> for Value {
960 fn from(v: String) -> Self {
961 Self::String(Arc::from(v))
962 }
963}
964
965impl From<Arc<str>> for Value {
966 fn from(v: Arc<str>) -> Self {
967 Self::String(v)
968 }
969}
970
971impl From<()> for Value {
972 /// The unit type maps to [`Null`](Self::Null) — the natural return for a
973 /// fire-and-forget external that produces no value.
974 fn from((): ()) -> Self {
975 Self::Null
976 }
977}
978
979// NS-A8: identity conversions from the glam compute types (T1 — "one
980// workspace-pinned glam version shared with bevy-brink → the bevy marshal is
981// identity on the same types"). A host binding returning `impl Into<Value>`
982// can hand back a `glam::Vec3` directly.
983
984impl From<glam::Vec2> for Value {
985 fn from(v: glam::Vec2) -> Self {
986 Self::Vec2(v)
987 }
988}
989
990impl From<glam::Vec3> for Value {
991 fn from(v: glam::Vec3) -> Self {
992 Self::Vec3(v)
993 }
994}
995
996impl From<glam::Vec4> for Value {
997 fn from(v: glam::Vec4) -> Self {
998 Self::Vec4(v)
999 }
1000}
1001
1002impl From<glam::Quat> for Value {
1003 fn from(v: glam::Quat) -> Self {
1004 Self::Quat(v)
1005 }
1006}
1007
1008impl From<glam::Mat2> for Value {
1009 fn from(v: glam::Mat2) -> Self {
1010 Self::Mat2(v)
1011 }
1012}
1013
1014impl From<glam::Mat3> for Value {
1015 fn from(v: glam::Mat3) -> Self {
1016 Self::Mat3(v)
1017 }
1018}
1019
1020impl From<glam::Mat4> for Value {
1021 fn from(v: glam::Mat4) -> Self {
1022 Self::Mat4(v)
1023 }
1024}
1025
1026/// An ink list value: a set of list items plus their origin list definitions.
1027#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1028pub struct ListValue {
1029 /// The active items in this list (each a `ListItem` `DefinitionId`).
1030 pub items: Vec<DefinitionId>,
1031 /// The origin list definitions this value was derived from.
1032 pub origins: Vec<DefinitionId>,
1033}
1034
1035/// A scalar key for a [`Value::Map`].
1036///
1037/// v1 permits `int`, `string`, and `bool` keys (value-model-spec §4). Keys are
1038/// compared for equality only — never hashed or sorted — because a
1039/// [`Value::Map`] iterates in insertion order. Two keys of different variants
1040/// are never equal (an `Int(1)` key and a `Bool(true)` key are distinct).
1041#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1042pub enum MapKey {
1043 /// An integer key.
1044 Int(i32),
1045 /// A string key.
1046 Str(Arc<str>),
1047 /// A boolean key.
1048 Bool(bool),
1049}
1050
1051impl MapKey {
1052 /// Derive a map key from a scalar [`Value`], or `None` if the value is not
1053 /// one of the permitted key types (`int`/`string`/`bool`).
1054 ///
1055 /// This is the seam the collection opcodes (T1b) use to turn an indexing
1056 /// operand into a key; keeping it here keeps the permitted key domain in
1057 /// one place.
1058 pub fn from_value(value: &Value) -> Option<Self> {
1059 match value {
1060 Value::Int(n) => Some(Self::Int(*n)),
1061 Value::String(s) => Some(Self::Str(Arc::clone(s))),
1062 Value::Bool(b) => Some(Self::Bool(*b)),
1063 _ => None,
1064 }
1065 }
1066}
1067
1068impl From<i32> for MapKey {
1069 fn from(v: i32) -> Self {
1070 Self::Int(v)
1071 }
1072}
1073
1074impl From<bool> for MapKey {
1075 fn from(v: bool) -> Self {
1076 Self::Bool(v)
1077 }
1078}
1079
1080impl From<&str> for MapKey {
1081 fn from(v: &str) -> Self {
1082 Self::Str(Arc::from(v))
1083 }
1084}
1085
1086impl From<String> for MapKey {
1087 fn from(v: String) -> Self {
1088 Self::Str(Arc::from(v))
1089 }
1090}
1091
1092impl From<Arc<str>> for MapKey {
1093 fn from(v: Arc<str>) -> Self {
1094 Self::Str(v)
1095 }
1096}
1097
1098/// The payload of [`Value::Map`]: an insertion-ordered map with scalar keys.
1099///
1100/// Backed by a flat `Vec` of `(key, value)` entries. Game-scale maps are
1101/// small, and a flat vector beats persistent/HAMT or hashed structures on both
1102/// constant factors and wasm size (value-model-spec §5). Iteration is
1103/// insertion order — the ratified ruling for v1 (§4) — so the structure is
1104/// deterministic without any sorting, and there is no `HashMap` to leak
1105/// iteration order.
1106///
1107/// `insert`/`remove` preserve insertion order: re-inserting an existing key
1108/// overwrites its value in place (keeping the key's original position), and
1109/// The payload of a [`Value::Weighted`] (NS-A7, `docs/stdlib-spec.md` §8):
1110/// positive-int weights over values, kept in construction order.
1111///
1112/// `PartialEq` is hand-written: equality is **multiset content** — the same
1113/// `(weight, value)` entries with the same multiplicities, regardless of
1114/// order (the #909 map content-over-form precedent applied to the F17
1115/// multiset: `weighted(3, "a", 1, "b") == weighted(1, "b", 3, "a")`).
1116/// Duplicate entries are legal and multiplicity-sensitive. Construction
1117/// order still governs display and the `roll` draw walk (deterministic
1118/// offset → entry mapping), exactly as map iteration order survives the
1119/// order-insensitive map equality. O(n²) matching — the accepted trade for
1120/// small, hand-written game-scale tables (same as `OrderedMap`).
1121#[derive(Debug, Clone, Serialize, Deserialize)]
1122pub struct WeightedValue {
1123 /// `(weight, value)` entries in construction order. Invariant (held by
1124 /// the only producer, `weighted_new`, and the wire reader): non-empty,
1125 /// every weight ≥ 1.
1126 pub entries: Vec<(i32, Value)>,
1127}
1128
1129impl WeightedValue {
1130 /// The total weight of the table as an `i64` (a sum of `i32` weights
1131 /// can exceed `i32::MAX`; the draw walks in `i64`).
1132 #[must_use]
1133 pub fn total_weight(&self) -> i64 {
1134 self.entries.iter().map(|(w, _)| i64::from(*w)).sum()
1135 }
1136}
1137
1138impl PartialEq for WeightedValue {
1139 fn eq(&self, other: &Self) -> bool {
1140 if self.entries.len() != other.entries.len() {
1141 return false;
1142 }
1143 let mut used = vec![false; other.entries.len()];
1144 'outer: for (w, v) in &self.entries {
1145 for (i, (ow, ov)) in other.entries.iter().enumerate() {
1146 if !used[i] && w == ow && v == ov {
1147 used[i] = true;
1148 continue 'outer;
1149 }
1150 }
1151 return false;
1152 }
1153 true
1154 }
1155}
1156
1157/// `remove` shifts later entries down. Lookups are linear; that is the
1158/// intended trade for small maps and stable ordering.
1159///
1160/// `PartialEq` is hand-written, not derived (issue #909, ruled 2026-07-18 —
1161/// `docs/decision-log.md` "Map/record equality is insertion-order-insensitive"):
1162/// equality is **content-based**, comparing key→value pairs regardless of
1163/// insertion order — `#{a:1,b:2} == #{b:2,a:1}` is `true`. Only equality
1164/// ignores order; [`iter`](Self::iter)/[`keys`](Self::keys)/[`values`](Self::values)
1165/// and every codec still walk `entries` in insertion order, unchanged. The
1166/// derived `PartialEq` this replaces compared `entries` as a `Vec`, which is
1167/// order-sensitive — the bug. `Value::Map`'s `Arc::ptr_eq` fast path (same
1168/// snapshot → instant `true`) lives one level up in `impl PartialEq for
1169/// Value`; this impl is the structural fallback it calls into.
1170#[derive(Debug, Clone, Default, Serialize)]
1171pub struct OrderedMap {
1172 entries: Vec<(MapKey, Value)>,
1173}
1174
1175impl<'de> Deserialize<'de> for OrderedMap {
1176 /// Hand-written, not derived (issue #985, follow-up to #909): the derived
1177 /// impl would deserialize `entries` verbatim as a `Vec<(MapKey, Value)>`,
1178 /// letting a crafted or corrupt payload carry a duplicate key and
1179 /// construct a map that violates the content-based `Eq` invariant above
1180 /// — `Eq` assumes each key appears at most once. This decodes into the
1181 /// same shape the derive would have produced, then walks the entries
1182 /// through the same duplicate-key check the `.inkb`/`.inkt`/transcript
1183 /// decoders use (rejecting rather than silently keeping the last
1184 /// occurrence — a legitimate encoder never emits a repeat, since
1185 /// `insert` de-duplicates on the write side, so a repeat is corrupt
1186 /// input, never a panic).
1187 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1188 where
1189 D: serde::Deserializer<'de>,
1190 {
1191 #[derive(Deserialize)]
1192 struct Shadow {
1193 entries: Vec<(MapKey, Value)>,
1194 }
1195
1196 let shadow = Shadow::deserialize(deserializer)?;
1197 let mut map = Self::with_capacity(shadow.entries.len());
1198 for (key, value) in shadow.entries {
1199 if map.contains_key(&key) {
1200 return Err(serde::de::Error::custom("duplicate key in map value"));
1201 }
1202 map.insert(key, value);
1203 }
1204 Ok(map)
1205 }
1206}
1207
1208impl PartialEq for OrderedMap {
1209 /// Content comparison: same number of entries, and every key in `self`
1210 /// maps to an equal value in `other`. Order-insensitive by construction
1211 /// (a lookup by key, not a positional walk) — the len check is a fast
1212 /// path (mismatched sizes can never be equal, and it makes the
1213 /// same-length-different-keys case cheap to reject), and the per-entry
1214 /// `get` gives each comparison the same `Value::eq` `Arc::ptr_eq`
1215 /// shortcuts as any other structural compare. `O(n)` entries, each a
1216 /// linear `get` — `O(n^2)` worst case, the accepted trade for small,
1217 /// game-scale maps (same trade `get`/`insert`/`remove` already make).
1218 fn eq(&self, other: &Self) -> bool {
1219 self.entries.len() == other.entries.len()
1220 && self.entries.iter().all(|(key, value)| {
1221 other
1222 .get(key)
1223 .is_some_and(|other_value| other_value == value)
1224 })
1225 }
1226}
1227
1228impl OrderedMap {
1229 /// Create an empty map.
1230 pub fn new() -> Self {
1231 Self {
1232 entries: Vec::new(),
1233 }
1234 }
1235
1236 /// Create an empty map with capacity for `n` entries.
1237 pub fn with_capacity(n: usize) -> Self {
1238 Self {
1239 entries: Vec::with_capacity(n),
1240 }
1241 }
1242
1243 /// Number of entries.
1244 pub fn len(&self) -> usize {
1245 self.entries.len()
1246 }
1247
1248 /// Whether the map has no entries.
1249 pub fn is_empty(&self) -> bool {
1250 self.entries.is_empty()
1251 }
1252
1253 /// Borrow the value for `key`, or `None` if absent.
1254 pub fn get(&self, key: &MapKey) -> Option<&Value> {
1255 self.entries.iter().find(|(k, _)| k == key).map(|(_, v)| v)
1256 }
1257
1258 /// Whether `key` is present.
1259 pub fn contains_key(&self, key: &MapKey) -> bool {
1260 self.entries.iter().any(|(k, _)| k == key)
1261 }
1262
1263 /// Mutably borrow the value for `key`, or `None` if absent. The
1264 /// intermediate-segment leg of the T1e projection RMW spine
1265 /// (`docs/t1e-spec.md` §3): recursing a `make_mut` chain through a map
1266 /// needs a mutable handle to an *existing* entry without touching
1267 /// insertion order, which [`insert`](Self::insert) alone (add-or-replace)
1268 /// can't provide.
1269 pub fn get_mut(&mut self, key: &MapKey) -> Option<&mut Value> {
1270 self.entries
1271 .iter_mut()
1272 .find(|(k, _)| k == key)
1273 .map(|(_, v)| v)
1274 }
1275
1276 /// Insert `value` under `key`, returning the previous value if the key was
1277 /// already present.
1278 ///
1279 /// An existing key keeps its insertion position (only its value changes);
1280 /// a new key is appended, so first-insertion order is preserved.
1281 pub fn insert(&mut self, key: MapKey, value: Value) -> Option<Value> {
1282 if let Some((_, slot)) = self.entries.iter_mut().find(|(k, _)| *k == key) {
1283 Some(core::mem::replace(slot, value))
1284 } else {
1285 self.entries.push((key, value));
1286 None
1287 }
1288 }
1289
1290 /// Remove `key`, returning its value if it was present. Later entries shift
1291 /// down, so insertion order among the survivors is preserved.
1292 pub fn remove(&mut self, key: &MapKey) -> Option<Value> {
1293 let idx = self.entries.iter().position(|(k, _)| k == key)?;
1294 Some(self.entries.remove(idx).1)
1295 }
1296
1297 /// Iterate `(key, value)` pairs in insertion order.
1298 pub fn iter(&self) -> impl Iterator<Item = (&MapKey, &Value)> {
1299 self.entries.iter().map(|(k, v)| (k, v))
1300 }
1301
1302 /// Iterate keys in insertion order.
1303 pub fn keys(&self) -> impl Iterator<Item = &MapKey> {
1304 self.entries.iter().map(|(k, _)| k)
1305 }
1306
1307 /// Iterate values in insertion order.
1308 pub fn values(&self) -> impl Iterator<Item = &Value> {
1309 self.entries.iter().map(|(_, v)| v)
1310 }
1311}
1312
1313impl FromIterator<(MapKey, Value)> for OrderedMap {
1314 /// Collect entries in order. If a key repeats, the last value wins while
1315 /// the key keeps its first-insertion position — matching [`insert`].
1316 ///
1317 /// [`insert`]: OrderedMap::insert
1318 fn from_iter<I: IntoIterator<Item = (MapKey, Value)>>(iter: I) -> Self {
1319 let mut map = Self::new();
1320 for (k, v) in iter {
1321 map.insert(k, v);
1322 }
1323 map
1324 }
1325}
1326
1327#[cfg(test)]
1328mod tests {
1329 use super::*;
1330 use crate::id::DefinitionTag;
1331
1332 #[test]
1333 fn value_type_discriminant() {
1334 assert_eq!(Value::Int(0).value_type(), ValueType::Int);
1335 assert_eq!(Value::Float(0.0).value_type(), ValueType::Float);
1336 assert_eq!(Value::Bool(true).value_type(), ValueType::Bool);
1337 assert_eq!(Value::String("".into()).value_type(), ValueType::String);
1338 assert_eq!(Value::Null.value_type(), ValueType::Null);
1339
1340 let list = ListValue {
1341 items: vec![],
1342 origins: vec![],
1343 };
1344 assert_eq!(Value::List(list.into()).value_type(), ValueType::List);
1345
1346 let target = DefinitionId::new(DefinitionTag::Address, 1);
1347 assert_eq!(
1348 Value::DivertTarget(target).value_type(),
1349 ValueType::DivertTarget
1350 );
1351 }
1352
1353 #[test]
1354 fn from_impls_roundtrip() {
1355 assert_eq!(Value::from(7_i32), Value::Int(7));
1356 assert_eq!(Value::from(1.5_f32), Value::Float(1.5));
1357 assert_eq!(Value::from(true), Value::Bool(true));
1358 assert_eq!(Value::from("hi"), Value::String("hi".into()));
1359 assert_eq!(Value::from(String::from("hi")), Value::String("hi".into()));
1360 assert_eq!(Value::from(()), Value::Null);
1361 }
1362
1363 #[test]
1364 fn accessors_are_strict_except_int_to_float() {
1365 assert_eq!(Value::Int(3).as_int(), Some(3));
1366 assert_eq!(Value::Float(3.0).as_int(), None);
1367 assert_eq!(Value::Bool(true).as_int(), None);
1368
1369 // int->float promotion is allowed (matches ink coercion);
1370 // float->int truncation is not.
1371 assert_eq!(Value::Int(3).as_float(), Some(3.0));
1372 assert_eq!(Value::Float(2.5).as_float(), Some(2.5));
1373
1374 assert_eq!(Value::Bool(true).as_bool(), Some(true));
1375 assert_eq!(Value::Int(1).as_bool(), None);
1376
1377 assert_eq!(Value::String("x".into()).as_str(), Some("x"));
1378 assert_eq!(Value::Int(1).as_str(), None);
1379 }
1380
1381 // ── Collections: value_type + constructors ─────────────────────────────
1382
1383 #[test]
1384 fn collection_value_types() {
1385 assert_eq!(
1386 Value::array(vec![Value::Int(1)]).value_type(),
1387 ValueType::Array
1388 );
1389 assert_eq!(Value::map(OrderedMap::new()).value_type(), ValueType::Map);
1390 }
1391
1392 #[test]
1393 fn array_accessors() {
1394 let v = Value::array(vec![Value::Int(1), Value::Int(2)]);
1395 let items = v.as_array().expect("is array");
1396 assert_eq!(items.len(), 2);
1397 assert!(Value::Int(0).as_array().is_none());
1398 assert!(v.as_map().is_none());
1399 }
1400
1401 // ── MapKey ─────────────────────────────────────────────────────────────
1402
1403 #[test]
1404 fn map_key_from_value_permitted_domain() {
1405 assert_eq!(MapKey::from_value(&Value::Int(3)), Some(MapKey::Int(3)));
1406 assert_eq!(
1407 MapKey::from_value(&Value::String("k".into())),
1408 Some(MapKey::Str("k".into()))
1409 );
1410 assert_eq!(
1411 MapKey::from_value(&Value::Bool(true)),
1412 Some(MapKey::Bool(true))
1413 );
1414 // Non-scalar / disallowed key types are rejected.
1415 assert_eq!(MapKey::from_value(&Value::Float(1.0)), None);
1416 assert_eq!(MapKey::from_value(&Value::Null), None);
1417 assert_eq!(MapKey::from_value(&Value::array(vec![])), None);
1418 }
1419
1420 #[test]
1421 fn map_key_variants_are_distinct() {
1422 // 1, "1", and true are three different keys even though they might
1423 // coerce to each other elsewhere in the VM.
1424 assert_ne!(MapKey::Int(1), MapKey::Bool(true));
1425 assert_ne!(MapKey::from(1), MapKey::from("1"));
1426 assert_ne!(MapKey::from(true), MapKey::from(false));
1427 assert_eq!(MapKey::from(1), MapKey::Int(1));
1428 assert_eq!(MapKey::from("a"), MapKey::Str("a".into()));
1429 }
1430
1431 // ── OrderedMap: insertion order, insert/get/remove ─────────────────────
1432
1433 #[test]
1434 fn ordered_map_preserves_insertion_order() {
1435 let mut m = OrderedMap::new();
1436 assert!(m.is_empty());
1437 m.insert(MapKey::from("b"), Value::Int(2));
1438 m.insert(MapKey::from("a"), Value::Int(1));
1439 m.insert(MapKey::from("c"), Value::Int(3));
1440 let keys: Vec<&MapKey> = m.keys().collect();
1441 assert_eq!(
1442 keys,
1443 vec![&MapKey::from("b"), &MapKey::from("a"), &MapKey::from("c")]
1444 );
1445 assert_eq!(m.len(), 3);
1446 assert_eq!(m.get(&MapKey::from("a")), Some(&Value::Int(1)));
1447 assert!(m.contains_key(&MapKey::from("c")));
1448 assert!(!m.contains_key(&MapKey::from("z")));
1449 }
1450
1451 #[test]
1452 fn ordered_map_reinsert_keeps_position_and_returns_old() {
1453 let mut m = OrderedMap::new();
1454 m.insert(MapKey::from("x"), Value::Int(1));
1455 m.insert(MapKey::from("y"), Value::Int(2));
1456 let old = m.insert(MapKey::from("x"), Value::Int(9));
1457 assert_eq!(old, Some(Value::Int(1)));
1458 // Order unchanged: x still first.
1459 let keys: Vec<&MapKey> = m.keys().collect();
1460 assert_eq!(keys, vec![&MapKey::from("x"), &MapKey::from("y")]);
1461 assert_eq!(m.get(&MapKey::from("x")), Some(&Value::Int(9)));
1462 }
1463
1464 #[test]
1465 fn ordered_map_remove_shifts_survivors() {
1466 let mut m = OrderedMap::new();
1467 m.insert(MapKey::from("a"), Value::Int(1));
1468 m.insert(MapKey::from("b"), Value::Int(2));
1469 m.insert(MapKey::from("c"), Value::Int(3));
1470 assert_eq!(m.remove(&MapKey::from("b")), Some(Value::Int(2)));
1471 assert_eq!(m.remove(&MapKey::from("b")), None);
1472 let keys: Vec<&MapKey> = m.keys().collect();
1473 assert_eq!(keys, vec![&MapKey::from("a"), &MapKey::from("c")]);
1474 }
1475
1476 #[test]
1477 fn ordered_map_from_iter_last_wins_first_position() {
1478 let m: OrderedMap = [
1479 (MapKey::from("a"), Value::Int(1)),
1480 (MapKey::from("b"), Value::Int(2)),
1481 (MapKey::from("a"), Value::Int(10)),
1482 ]
1483 .into_iter()
1484 .collect();
1485 assert_eq!(m.len(), 2);
1486 let keys: Vec<&MapKey> = m.keys().collect();
1487 assert_eq!(keys, vec![&MapKey::from("a"), &MapKey::from("b")]);
1488 assert_eq!(m.get(&MapKey::from("a")), Some(&Value::Int(10)));
1489 }
1490
1491 // ── OrderedMap::deserialize: duplicate-key rejection (#985, follow-up to
1492 // #909) ──────────────────────────────────────────────────────────────
1493 //
1494 // `OrderedMap`'s `Eq` is content-based and assumes each key appears at
1495 // most once. A legitimate `Serialize` never emits a duplicate key —
1496 // `insert` de-duplicates on the write side — so a JSON payload with a
1497 // repeated key only ever arises from a hand-crafted or corrupted save/
1498 // journal file (the serde deserialize boundary `Story::load_state` and
1499 // friends go through). The hand-written `Deserialize` below must reject
1500 // it with a decode error, never silently keep the last occurrence and
1501 // hand back a map that violates the invariant its `Eq` relies on.
1502
1503 #[test]
1504 fn ordered_map_deserialize_rejects_duplicate_key() {
1505 let json = r#"{"entries":[[{"Str":"a"},{"Int":1}],[{"Str":"a"},{"Int":2}]]}"#;
1506 let err = serde_json::from_str::<OrderedMap>(json)
1507 .expect_err("duplicate key must not deserialize");
1508 assert!(
1509 err.to_string().contains("duplicate key"),
1510 "unexpected error: {err}"
1511 );
1512 }
1513
1514 #[test]
1515 fn ordered_map_deserialize_accepts_distinct_keys() {
1516 let json = r#"{"entries":[[{"Str":"a"},{"Int":1}],[{"Str":"b"},{"Int":2}]]}"#;
1517 let m: OrderedMap = serde_json::from_str(json).expect("distinct keys must deserialize");
1518 assert_eq!(m.len(), 2);
1519 assert_eq!(m.get(&MapKey::from("a")), Some(&Value::Int(1)));
1520 assert_eq!(m.get(&MapKey::from("b")), Some(&Value::Int(2)));
1521 }
1522
1523 #[test]
1524 fn ordered_map_serde_json_round_trip_without_duplicates() {
1525 let mut m = OrderedMap::new();
1526 m.insert(MapKey::from("hp"), Value::Int(10));
1527 m.insert(MapKey::from(true), Value::String("flag".into()));
1528 m.insert(MapKey::from(7), Value::Float(1.5));
1529 let json = serde_json::to_string(&m).expect("serialize");
1530 let back: OrderedMap = serde_json::from_str(&json).expect("deserialize");
1531 assert_eq!(back, m);
1532 }
1533
1534 // A crafted `Value::Map` payload (the shape a `SaveState`/journal decode
1535 // actually walks) must reject the same way as the bare `OrderedMap` case
1536 // above — the duplicate-key check has to fire through `Value`'s derived
1537 // `Deserialize` too, not just when `OrderedMap` is deserialized directly.
1538 // ── NS-A8 tower: equality + serde lane discipline ──────────────────
1539
1540 #[test]
1541 fn tower_equality_is_componentwise_ieee() {
1542 let a = Value::Vec2(glam::Vec2::new(1.0, 2.0));
1543 assert_eq!(a, Value::Vec2(glam::Vec2::new(1.0, 2.0)));
1544 // -0 == +0 per lane; a NaN lane never equals itself (T4).
1545 assert_eq!(
1546 Value::Vec2(glam::Vec2::new(-0.0, 1.0)),
1547 Value::Vec2(glam::Vec2::new(0.0, 1.0))
1548 );
1549 let nan = Value::Vec3(glam::Vec3::new(f32::NAN, 0.0, 0.0));
1550 assert_ne!(nan.clone(), nan);
1551 // Cross-kind is plain inequality at the value layer.
1552 assert_ne!(a, Value::Vec3(glam::Vec3::new(1.0, 2.0, 0.0)));
1553 }
1554
1555 /// T5: the serde form is the flat lane array — explicit lanes
1556 /// (column-major for matrices), never glam's memory representation.
1557 #[test]
1558 fn tower_serde_is_flat_lane_arrays() {
1559 let v = Value::Vec3(glam::Vec3::new(1.0, 2.5, -3.0));
1560 let json = serde_json::to_string(&v).expect("serialize");
1561 assert_eq!(json, r#"{"Vec3":[1.0,2.5,-3.0]}"#);
1562 let back: Value = serde_json::from_str(&json).expect("deserialize");
1563 assert_eq!(back, v);
1564
1565 let m = Value::Mat2(glam::Mat2::from_cols_array(&[1.0, 2.0, 3.0, 4.0]));
1566 let json = serde_json::to_string(&m).expect("serialize");
1567 assert_eq!(json, r#"{"Mat2":[1.0,2.0,3.0,4.0]}"#);
1568 let back: Value = serde_json::from_str(&json).expect("deserialize");
1569 assert_eq!(back, m);
1570
1571 let q = Value::Quat(glam::Quat::from_xyzw(0.1, 0.2, 0.3, 0.4));
1572 let back: Value = serde_json::from_str(&serde_json::to_string(&q).expect("serialize"))
1573 .expect("deserialize");
1574 assert_eq!(back, q);
1575 }
1576
1577 #[test]
1578 fn tower_accessors_and_from_impls_are_identity() {
1579 let v = glam::Vec3::new(1.0, 2.0, 3.0);
1580 assert_eq!(Value::from(v).as_vec3(), Some(v));
1581 assert_eq!(Value::from(v).as_vec2(), None);
1582 let m = glam::Mat4::IDENTITY;
1583 assert_eq!(Value::from(m).as_mat4(), Some(m));
1584 assert_eq!(Value::Int(1).as_quat(), None);
1585 }
1586
1587 #[test]
1588 fn value_map_deserialize_rejects_duplicate_key() {
1589 let json = r#"{"Map":{"entries":[[{"Str":"a"},{"Int":1}],[{"Str":"a"},{"Int":2}]]}}"#;
1590 let err =
1591 serde_json::from_str::<Value>(json).expect_err("duplicate key must not deserialize");
1592 assert!(
1593 err.to_string().contains("duplicate key"),
1594 "unexpected error: {err}"
1595 );
1596 }
1597
1598 // ── Copy-on-write mechanics (take → make_mut → write-back) ──────────────
1599
1600 #[test]
1601 fn clone_is_arc_bump_not_deep_copy() {
1602 let v = Value::array(vec![Value::Int(1)]);
1603 let arc = Arc::clone(v.as_array().expect("array"));
1604 assert_eq!(Arc::strong_count(&arc), 2); // v + arc
1605 let v2 = v.clone();
1606 assert_eq!(Arc::strong_count(&arc), 3); // v + v2 + arc
1607 drop(v2);
1608 assert_eq!(Arc::strong_count(&arc), 2);
1609 }
1610
1611 #[test]
1612 fn array_make_mut_in_place_when_unique() {
1613 let mut v = Value::array(vec![Value::Int(1)]);
1614 // Unique Arc: `make_mut` returns the same allocation, no COW copy.
1615 // (Compare the Arc allocation address, not the Vec's data buffer,
1616 // which may move when `push` grows capacity.)
1617 let arc_before = Arc::as_ptr(v.as_array().expect("array"));
1618 v.array_make_mut().expect("array").push(Value::Int(2));
1619 let arc_after = Arc::as_ptr(v.as_array().expect("array"));
1620 assert_eq!(arc_before, arc_after, "unique Arc mutates in place");
1621 assert_eq!(v.as_array().expect("array").len(), 2);
1622 }
1623
1624 #[test]
1625 fn array_make_mut_copies_when_shared() {
1626 let original = Value::array(vec![Value::Int(1)]);
1627 let mut copy = original.clone(); // shares the Arc
1628 // Mutate the copy: COW must fork so `original` is untouched.
1629 copy.array_make_mut().expect("array").push(Value::Int(2));
1630 assert_eq!(
1631 original.as_array().expect("array").as_slice(),
1632 &[Value::Int(1)]
1633 );
1634 assert_eq!(copy.as_array().expect("array").len(), 2);
1635 // After the fork both are unique again.
1636 assert_eq!(Arc::strong_count(original.as_array().expect("array")), 1);
1637 }
1638
1639 #[test]
1640 fn map_make_mut_copies_when_shared() {
1641 let mut base = OrderedMap::new();
1642 base.insert(MapKey::from("a"), Value::Int(1));
1643 let original = Value::map(base);
1644 let mut copy = original.clone();
1645 copy.map_make_mut()
1646 .expect("map")
1647 .insert(MapKey::from("b"), Value::Int(2));
1648 assert_eq!(original.as_map().expect("map").len(), 1);
1649 assert_eq!(copy.as_map().expect("map").len(), 2);
1650 }
1651
1652 #[test]
1653 fn make_mut_returns_none_for_non_collection() {
1654 assert!(Value::Int(1).array_make_mut().is_none());
1655 assert!(Value::Int(1).map_make_mut().is_none());
1656 }
1657
1658 /// `record_make_mut` gets the same COW proof as `array_make_mut`/
1659 /// `map_make_mut` above — issue #1476's audit found this variant was the
1660 /// one collection `make_mut` without a dedicated "copies when shared"
1661 /// regression, despite sharing the exact take → `make_mut` → write-back
1662 /// discipline (the COW discipline documented on [`Value`] —
1663 /// `docs/value-model-spec.md` §4/§5).
1664 #[test]
1665 fn record_make_mut_copies_when_shared() {
1666 let shape = ShapeId(0);
1667 let original = Value::record(shape, vec![Value::Int(1), Value::Int(2)]);
1668 let mut copy = original.clone(); // shares the Arc
1669 copy.record_make_mut().expect("record")[0] = Value::Int(99);
1670 assert_eq!(
1671 original.as_record().expect("record").1.as_slice(),
1672 &[Value::Int(1), Value::Int(2)],
1673 "mutating the copy must never be observable through the original"
1674 );
1675 assert_eq!(
1676 copy.as_record().expect("record").1.as_slice(),
1677 &[Value::Int(99), Value::Int(2)]
1678 );
1679 // After the fork both are unique again.
1680 assert_eq!(
1681 Arc::strong_count(original.as_record().expect("record").1),
1682 1
1683 );
1684 }
1685
1686 /// The classic nested-collection leak site (issue #1476), one layer
1687 /// deeper: a `Record` field itself holds an `Array` (two independent
1688 /// `Arc`s stacked — the record's field vec, and the array's backing
1689 /// vec). `let y = x` then mutating `x`'s field-array in place must never
1690 /// surface through `y`, exactly like a bare `Array`-of-`Array`
1691 /// (`rmw-mutator-shared-nested-lvalue`, `tests/tier1-brink/`).
1692 ///
1693 /// The obvious source form — `STRUCT Bag = #{ items: Array<int> }`,
1694 /// `push(a.items, 3)` — used to fault instead of reaching this code path:
1695 /// `push`/`insert`/`remove`'s bare-lvalue fast path (`try_lower_mutator_stmt`
1696 /// in `brink-ir::lir::lower::blocks`) treated *any* `hir::Expr::Path`
1697 /// lvalue, including a multi-segment TM-4b dotted field-access path like
1698 /// `a.items`, as a single bare-variable target and resolved the whole
1699 /// path's range to its root symbol, applying the mutator to `a` itself (a
1700 /// `Record`) instead of `a`'s `items` field. That was issue #1495, fixed
1701 /// by routing a single-level struct-field mutator lvalue through the new
1702 /// `lower_field_mutator` (mirrors `try_lower_field_assignment`'s existing
1703 /// `path.segments.len() > 1` split). This `Value`-layer test remains as
1704 /// the isolation guarantee's own regression pin; the end-to-end
1705 /// aliasing case now lives at
1706 /// `tests/tier1-brink/struct-field-mutator-lvalue/story.ink` alongside
1707 /// the fix.
1708 #[test]
1709 fn nested_array_inside_record_field_is_isolated_after_copy() {
1710 let shape = ShapeId(0);
1711 let inner = Value::array(vec![Value::Int(1), Value::Int(2)]);
1712 let original = Value::record(shape, vec![Value::String("bag".into()), inner]);
1713 let mut copy = original.clone(); // shares both Arcs (record + inner array)
1714
1715 // Take → make_mut → write-back on the copy's inner array field,
1716 // mirroring `collection_ops`'s RMW discipline: pull the field out,
1717 // COW-mutate it, write it back into the (already-uniqued) record.
1718 let fields = copy.record_make_mut().expect("record");
1719 let mut inner_copy = fields[1].clone();
1720 inner_copy
1721 .array_make_mut()
1722 .expect("array")
1723 .push(Value::Int(3));
1724 fields[1] = inner_copy;
1725
1726 let original_inner = original
1727 .as_record()
1728 .expect("record")
1729 .1
1730 .get(1)
1731 .expect("field 1")
1732 .as_array()
1733 .expect("array");
1734 assert_eq!(
1735 original_inner.as_slice(),
1736 &[Value::Int(1), Value::Int(2)],
1737 "mutating the copy's nested array must never be observable through the original record"
1738 );
1739 let copy_inner = copy
1740 .as_record()
1741 .expect("record")
1742 .1
1743 .get(1)
1744 .expect("field 1")
1745 .as_array()
1746 .expect("array");
1747 assert_eq!(
1748 copy_inner.as_slice(),
1749 &[Value::Int(1), Value::Int(2), Value::Int(3)]
1750 );
1751 }
1752
1753 /// Issue #1476's audit traced `Closure` val-capture (`vm.rs::MakeClosure`),
1754 /// `OptionVal(Some(Arc<Value>))`, and `Weighted` and found each correct by
1755 /// construction — same Arc-COW mechanics as `Array`/`Map`/`Record`, no
1756 /// bespoke mutation path exists for any of them — but flagged that none
1757 /// had a dedicated aliasing regression pinning it, the way
1758 /// `record_make_mut_copies_when_shared` does for records. These three
1759 /// close that gap (folded into #1508 per #1476's review comment); the
1760 /// fourth deferred test — the `as`-binding capture itself — still needs
1761 /// the `.inkb` v6 Choice captured-environment slot (#1684/#1508) and
1762 /// cannot be written yet.
1763 ///
1764 /// A `val` closure-env entry snapshots the bound value at
1765 /// `MakeClosure` time (`ClosureEnvEntry { is_ref: false, .. }`): the
1766 /// snapshot is an ordinary `Value` clone, so it shares the source
1767 /// array's `Arc` until something mutates one side. Mutating the
1768 /// *original* variable after capture must fork via COW, leaving the
1769 /// closure's captured snapshot untouched.
1770 ///
1771 /// This pins the `Value`-layer COW invariant that `MakeClosure`'s
1772 /// val-capture relies on; it hand-builds the `ClosureEnvEntry` directly
1773 /// and does not itself drive `vm.rs::MakeClosure` (`brink-format` cannot
1774 /// reach the VM) — no test here asserts that the opcode handler builds
1775 /// `is_ref: false` snapshot entries the way it does at `vm.rs:906-915`.
1776 #[test]
1777 fn closure_val_capture_is_isolated_from_later_mutation_of_the_source() {
1778 let mut original = Value::array(vec![Value::Int(1)]);
1779 let entry = ClosureEnvEntry {
1780 name: NameId(0),
1781 is_ref: false,
1782 payload: original.clone(), // val-capture snapshot, shares the Arc
1783 };
1784 let closure = Value::closure(DefinitionId::new(DefinitionTag::Address, 0), vec![entry]);
1785
1786 // Mutate the source *after* the closure captured it.
1787 original
1788 .array_make_mut()
1789 .expect("array")
1790 .push(Value::Int(2));
1791 assert_eq!(
1792 original.as_array().expect("array").as_slice(),
1793 &[Value::Int(1), Value::Int(2)]
1794 );
1795
1796 let captured = &closure.as_closure().expect("closure").env[0].payload;
1797 assert_eq!(
1798 captured.as_array().expect("array").as_slice(),
1799 &[Value::Int(1)],
1800 "mutating the source after MakeClosure must never be observable through the val-captured snapshot"
1801 );
1802 }
1803
1804 /// `OptionVal(Some(Arc<Value>))` (NS-A1) wraps its inner value behind its
1805 /// own `Arc`, but the inner `Value` itself may still share a *nested*
1806 /// Arc (e.g. an `Array`'s backing `Vec`) with whatever produced it. The
1807 /// same COW discipline must hold one layer down: mutating the source
1808 /// array after `some(...)` wrapped a clone of it must not leak through.
1809 #[test]
1810 fn option_some_wrap_is_isolated_from_later_mutation_of_the_source() {
1811 let mut original = Value::array(vec![Value::Int(1)]);
1812 let wrapped = Value::some(original.clone()); // shares the inner Arc
1813
1814 original
1815 .array_make_mut()
1816 .expect("array")
1817 .push(Value::Int(2));
1818 assert_eq!(
1819 original.as_array().expect("array").as_slice(),
1820 &[Value::Int(1), Value::Int(2)]
1821 );
1822
1823 let inner = wrapped
1824 .as_option()
1825 .expect("option")
1826 .expect("some")
1827 .as_array()
1828 .expect("array");
1829 assert_eq!(
1830 inner.as_slice(),
1831 &[Value::Int(1)],
1832 "mutating the source after `some(..)` wrapped it must never be observable through the wrapped copy"
1833 );
1834 }
1835
1836 /// `Weighted` (NS-A7) entries hold `Value`s in construction order; a
1837 /// table built from an `Array` entry shares that array's Arc with its
1838 /// source the same way a closure `val` capture or `some(..)` wrap does.
1839 /// Mutating the source after the table was built must not leak into the
1840 /// entry the table already captured.
1841 #[test]
1842 fn weighted_entry_capture_is_isolated_from_later_mutation_of_the_source() {
1843 let mut original = Value::array(vec![Value::Int(1)]);
1844 let table = Value::weighted(vec![(1, original.clone())]);
1845
1846 original
1847 .array_make_mut()
1848 .expect("array")
1849 .push(Value::Int(2));
1850 assert_eq!(
1851 original.as_array().expect("array").as_slice(),
1852 &[Value::Int(1), Value::Int(2)]
1853 );
1854
1855 let weighted = table.as_weighted().expect("weighted");
1856 let entry = weighted.entries[0].1.as_array().expect("array");
1857 assert_eq!(
1858 entry.as_slice(),
1859 &[Value::Int(1)],
1860 "mutating the source after the table captured it must never be observable through the weighted entry"
1861 );
1862 }
1863
1864 // ── Structural equality with the ptr_eq fast path ──────────────────────
1865
1866 #[test]
1867 fn array_equality_is_structural_across_distinct_arcs() {
1868 let a = Value::array(vec![Value::Int(1), Value::Int(2)]);
1869 let b = Value::array(vec![Value::Int(1), Value::Int(2)]);
1870 // Distinct Arcs, equal contents.
1871 assert!(!Arc::ptr_eq(a.as_array().unwrap(), b.as_array().unwrap()));
1872 assert_eq!(a, b);
1873 let c = Value::array(vec![Value::Int(1), Value::Int(3)]);
1874 assert_ne!(a, c);
1875 }
1876
1877 #[test]
1878 fn nested_collection_equality() {
1879 let inner = Value::array(vec![Value::Int(1)]);
1880 let a = Value::array(vec![inner.clone(), Value::map(OrderedMap::new())]);
1881 let b = Value::array(vec![
1882 Value::array(vec![Value::Int(1)]),
1883 Value::map(OrderedMap::new()),
1884 ]);
1885 assert_eq!(a, b);
1886 }
1887
1888 #[test]
1889 fn shared_snapshot_is_equal_via_ptr_eq() {
1890 let a = Value::array(vec![Value::Int(1)]);
1891 let snapshot = a.clone(); // same Arc
1892 assert!(Arc::ptr_eq(
1893 a.as_array().unwrap(),
1894 snapshot.as_array().unwrap()
1895 ));
1896 assert_eq!(a, snapshot);
1897 }
1898
1899 #[test]
1900 fn distinct_nan_arrays_never_equal_but_same_snapshot_is() {
1901 let a = Value::array(vec![Value::Float(f32::NAN)]);
1902 let b = Value::array(vec![Value::Float(f32::NAN)]);
1903 // Different Arcs: structural compare hits NaN != NaN.
1904 assert_ne!(a, b);
1905 // Same Arc (snapshot): ptr_eq fast path wins — equal even with NaN.
1906 // The spec calls this out as harmless and stated (§4).
1907 let snapshot = a.clone();
1908 assert_eq!(a, snapshot);
1909 }
1910
1911 #[test]
1912 fn map_equality_is_content_based_insertion_order_insensitive() {
1913 // Issue #909, ruled 2026-07-18: #{a:1,b:2} == #{b:2,a:1} is TRUE.
1914 // Two maps with the same key/value pairs inserted in different
1915 // orders are the same value — equality ignores insertion order even
1916 // though iteration/serialization order still follows it.
1917 let m1: OrderedMap = [
1918 (MapKey::from("a"), Value::Int(1)),
1919 (MapKey::from("b"), Value::Int(2)),
1920 ]
1921 .into_iter()
1922 .collect();
1923 let m2: OrderedMap = [
1924 (MapKey::from("b"), Value::Int(2)),
1925 (MapKey::from("a"), Value::Int(1)),
1926 ]
1927 .into_iter()
1928 .collect();
1929 // Both directions — PartialEq::eq isn't assumed symmetric by the
1930 // impl, so both orderings of the comparison are checked explicitly.
1931 assert_eq!(Value::map(m1.clone()), Value::map(m2.clone()));
1932 assert_eq!(Value::map(m2.clone()), Value::map(m1.clone()));
1933 assert_eq!(Value::map(m1.clone()), Value::map(m1.clone()));
1934
1935 // Iteration order is unaffected by the equality ruling — each map
1936 // still yields its own entries in the order they were inserted.
1937 assert_eq!(
1938 m1.keys().cloned().collect::<Vec<_>>(),
1939 vec![MapKey::from("a"), MapKey::from("b")]
1940 );
1941 assert_eq!(
1942 m2.keys().cloned().collect::<Vec<_>>(),
1943 vec![MapKey::from("b"), MapKey::from("a")]
1944 );
1945 }
1946
1947 #[test]
1948 fn map_equality_still_rejects_different_content() {
1949 // Content-based equality must still distinguish maps that genuinely
1950 // differ — same key count, different values; and different key
1951 // counts entirely (exercises the len fast-path).
1952 let a: OrderedMap = [(MapKey::from("a"), Value::Int(1))].into_iter().collect();
1953 let b: OrderedMap = [(MapKey::from("a"), Value::Int(2))].into_iter().collect();
1954 assert_ne!(Value::map(a.clone()), Value::map(b));
1955
1956 let c: OrderedMap = [
1957 (MapKey::from("a"), Value::Int(1)),
1958 (MapKey::from("b"), Value::Int(2)),
1959 ]
1960 .into_iter()
1961 .collect();
1962 assert_ne!(Value::map(a), Value::map(c));
1963 }
1964
1965 #[test]
1966 fn nested_map_equality_is_order_insensitive_at_every_level() {
1967 // A map value nested inside another map/array is compared through
1968 // the same content-based rule, recursively — reordering the inner
1969 // map's keys must not change the outer value's equality.
1970 let inner1: OrderedMap = [
1971 (MapKey::from("x"), Value::Int(1)),
1972 (MapKey::from("y"), Value::Int(2)),
1973 ]
1974 .into_iter()
1975 .collect();
1976 let inner2: OrderedMap = [
1977 (MapKey::from("y"), Value::Int(2)),
1978 (MapKey::from("x"), Value::Int(1)),
1979 ]
1980 .into_iter()
1981 .collect();
1982
1983 let outer1: OrderedMap = [
1984 (MapKey::from("inner"), Value::map(inner1)),
1985 (MapKey::from("other"), Value::Int(9)),
1986 ]
1987 .into_iter()
1988 .collect();
1989 let outer2: OrderedMap = [
1990 (MapKey::from("other"), Value::Int(9)),
1991 (MapKey::from("inner"), Value::map(inner2)),
1992 ]
1993 .into_iter()
1994 .collect();
1995 assert_eq!(Value::map(outer1), Value::map(outer2));
1996 }
1997
1998 #[test]
1999 fn record_equality_is_unaffected_by_map_ordering_ruling() {
2000 // Records are shape-ordered, not insertion-ordered (fields have a
2001 // fixed position from the closed shape) — the map ruling must not
2002 // change record equality, which stays a positional field compare
2003 // gated on matching `ShapeId`. Field order can't be reordered
2004 // through the public API, so this locks the existing behavior
2005 // rather than exercising a new order-insensitivity path.
2006 let shape = ShapeId(0);
2007 let r1 = Value::record(shape, vec![Value::Int(1), Value::Int(2)]);
2008 let r2 = Value::record(shape, vec![Value::Int(1), Value::Int(2)]);
2009 let r3 = Value::record(shape, vec![Value::Int(2), Value::Int(1)]);
2010 assert_eq!(r1, r2);
2011 assert_ne!(r1, r3);
2012 }
2013
2014 #[test]
2015 fn cross_type_inequality_unaffected() {
2016 // The hand-written PartialEq must keep the derive's cross-variant
2017 // behavior: different variants are never equal.
2018 assert_ne!(Value::Int(1), Value::Bool(true));
2019 assert_ne!(Value::array(vec![]), Value::Null);
2020 assert_ne!(Value::array(vec![]), Value::map(OrderedMap::new()));
2021 assert_eq!(Value::Null, Value::Null);
2022 }
2023
2024 // ── Tree serialization (T1a-3 / #525) ──────────────────────────────────
2025 //
2026 // SaveState and the session journal serialize `Value` through its derived
2027 // serde representation (BTreeMap<String, Value> globals; tagged event
2028 // payloads). These tests lock the *tree* round-trip for the collection
2029 // variants: an `Array`/`Map` serializes to a nested structure and comes
2030 // back structurally equal, with insertion order and scalar key types
2031 // preserved. Sharing is deliberately not preserved on the wire (spec §5) —
2032 // a snapshot serializes as a plain tree.
2033
2034 /// Round-trip a value through `serde_json` and assert structural equality.
2035 fn json_round_trip(v: &Value) -> Value {
2036 let json = serde_json::to_string(v).expect("serialize");
2037 serde_json::from_str(&json).expect("deserialize")
2038 }
2039
2040 #[test]
2041 fn scalar_serde_round_trip_unchanged() {
2042 for v in [
2043 Value::Int(-7),
2044 Value::Float(1.5),
2045 Value::Bool(true),
2046 Value::String("hi".into()),
2047 Value::Null,
2048 ] {
2049 assert_eq!(json_round_trip(&v), v);
2050 }
2051 }
2052
2053 #[test]
2054 fn array_serde_round_trip_is_structural() {
2055 let v = Value::array(vec![
2056 Value::Int(1),
2057 Value::String("two".into()),
2058 Value::Bool(false),
2059 ]);
2060 let back = json_round_trip(&v);
2061 assert_eq!(back, v);
2062 assert_eq!(back.value_type(), ValueType::Array);
2063 }
2064
2065 #[test]
2066 fn map_serde_round_trip_preserves_order_and_key_types() {
2067 // Mixed scalar key types and a deliberately non-sorted insertion order.
2068 let m: OrderedMap = [
2069 (MapKey::from("z"), Value::Int(1)),
2070 (MapKey::from(10), Value::Int(2)),
2071 (MapKey::from(true), Value::Int(3)),
2072 (MapKey::from("a"), Value::Int(4)),
2073 ]
2074 .into_iter()
2075 .collect();
2076 let v = Value::map(m);
2077 let back = json_round_trip(&v);
2078 // Structural equality is order-sensitive, so this also proves the wire
2079 // form preserved insertion order and each key's variant.
2080 assert_eq!(back, v);
2081 let back_map = back.as_map().expect("map");
2082 let keys: Vec<&MapKey> = back_map.keys().collect();
2083 assert_eq!(
2084 keys,
2085 vec![
2086 &MapKey::from("z"),
2087 &MapKey::from(10),
2088 &MapKey::from(true),
2089 &MapKey::from("a"),
2090 ]
2091 );
2092 }
2093
2094 #[test]
2095 fn nested_collection_serde_round_trip() {
2096 // An array of maps of arrays — the recursive tree case.
2097 let inner_map: OrderedMap = [
2098 (
2099 MapKey::from("items"),
2100 Value::array(vec![Value::Int(1), Value::Int(2)]),
2101 ),
2102 (MapKey::from("name"), Value::String("goblin".into())),
2103 ]
2104 .into_iter()
2105 .collect();
2106 let v = Value::array(vec![
2107 Value::map(inner_map),
2108 Value::array(vec![Value::map(OrderedMap::new())]),
2109 Value::Null,
2110 ]);
2111 assert_eq!(json_round_trip(&v), v);
2112 }
2113
2114 // ── Handle (T1d, docs/t1d-spec.md §2/§6) ────────────────────────────────
2115
2116 #[test]
2117 fn handle_value_type_and_constructor() {
2118 let h = Value::handle(NameId(3), 42);
2119 assert_eq!(h.value_type(), ValueType::Handle);
2120 assert_eq!(h.as_handle(), Some((NameId(3), 42)));
2121 assert!(Value::Int(0).as_handle().is_none());
2122 }
2123
2124 #[test]
2125 fn handle_equality_is_token_equality() {
2126 // Same kind, same id: equal.
2127 assert_eq!(Value::handle(NameId(1), 42), Value::handle(NameId(1), 42));
2128 // Same id, different kind: not equal — kind is part of the token.
2129 assert_ne!(Value::handle(NameId(1), 42), Value::handle(NameId(2), 42));
2130 // Same kind, different id: not equal.
2131 assert_ne!(Value::handle(NameId(1), 1), Value::handle(NameId(1), 2));
2132 // A Handle is never equal to any other variant, even with a
2133 // coincidentally matching id (compare against a DivertTarget encoding
2134 // the same raw bits).
2135 assert_ne!(
2136 Value::handle(NameId(1), 42),
2137 Value::DivertTarget(DefinitionId::new(DefinitionTag::Address, 42))
2138 );
2139 }
2140
2141 #[test]
2142 fn handle_is_not_a_legal_map_key() {
2143 // MapKey's domain is int/string/bool (value-model-spec §4); Handle
2144 // has no `MapKey::from_value` arm and falls through to `None`.
2145 assert_eq!(MapKey::from_value(&Value::handle(NameId(1), 42)), None);
2146 }
2147
2148 #[test]
2149 fn handle_serde_round_trip_is_structural() {
2150 let v = Value::handle(NameId(7), u64::MAX);
2151 let back = json_round_trip(&v);
2152 assert_eq!(back, v);
2153 assert_eq!(back.value_type(), ValueType::Handle);
2154 assert_eq!(back.as_handle(), Some((NameId(7), u64::MAX)));
2155 }
2156
2157 #[test]
2158 fn handle_nested_in_collection_serde_round_trip() {
2159 let v = Value::array(vec![
2160 Value::handle(NameId(1), 1),
2161 Value::handle(NameId(2), 2),
2162 Value::Null,
2163 ]);
2164 assert_eq!(json_round_trip(&v), v);
2165 }
2166
2167 // ── Option (NS-A1, docs/stdlib-spec.md §1.1/§1.4) ───────────────────
2168
2169 #[test]
2170 fn option_value_type_and_constructors() {
2171 assert_eq!(Value::none().value_type(), ValueType::Option);
2172 assert_eq!(Value::some(Value::Int(3)).value_type(), ValueType::Option);
2173 assert_eq!(Value::none().as_option(), Some(None));
2174 assert_eq!(
2175 Value::some(Value::Int(3)).as_option(),
2176 Some(Some(&Value::Int(3)))
2177 );
2178 assert_eq!(Value::Int(3).as_option(), None);
2179 }
2180
2181 #[test]
2182 fn option_equality_is_structural() {
2183 assert_eq!(Value::none(), Value::none());
2184 assert_eq!(Value::some(Value::Int(1)), Value::some(Value::Int(1)));
2185 assert_ne!(Value::some(Value::Int(1)), Value::some(Value::Int(2)));
2186 assert_ne!(Value::some(Value::Int(1)), Value::none());
2187 // The ruled `Option[T] ≠ T` strictness at the value layer: a
2188 // wrapped value is never equal to its bare form.
2189 assert_ne!(Value::some(Value::Int(1)), Value::Int(1));
2190 assert_ne!(Value::none(), Value::Null);
2191 }
2192
2193 #[test]
2194 fn option_nesting_is_preserved() {
2195 // some(none) is a real value, distinct from none — the enum shape
2196 // nests like any parameterized builtin.
2197 let some_none = Value::some(Value::none());
2198 assert_ne!(some_none, Value::none());
2199 assert_eq!(some_none, Value::some(Value::none()));
2200 }
2201
2202 #[test]
2203 fn option_clone_is_arc_bump() {
2204 let v = Value::some(Value::array(vec![Value::Int(1)]));
2205 let v2 = v.clone();
2206 let (Value::OptionVal(Some(a)), Value::OptionVal(Some(b))) = (&v, &v2) else {
2207 unreachable!("both are freshly built some values");
2208 };
2209 assert!(Arc::ptr_eq(a, b), "clone shares the payload Arc");
2210 }
2211
2212 #[test]
2213 fn option_serde_round_trip_is_structural() {
2214 for v in [
2215 Value::none(),
2216 Value::some(Value::Int(7)),
2217 Value::some(Value::none()),
2218 Value::array(vec![Value::none(), Value::some(Value::from("x"))]),
2219 ] {
2220 assert_eq!(json_round_trip(&v), v);
2221 }
2222 }
2223
2224 // ── NS-A5 `Value::Range` (F7, docs/stdlib-spec.md §7) ──────────────────
2225
2226 #[test]
2227 fn range_value_type_and_accessors() {
2228 let r = Value::range(1, 6, true);
2229 assert_eq!(r.value_type(), ValueType::Range);
2230 assert_eq!(r.as_range(), Some((1, 6, true)));
2231 assert_eq!(Value::Int(1).as_range(), None);
2232 assert_eq!(r.range_end_exclusive(), Some(7));
2233 assert_eq!(Value::range(1, 7, false).range_end_exclusive(), Some(7));
2234 assert_eq!(r.range_len(), Some(6));
2235 assert_eq!(Value::range(0, 0, false).range_len(), Some(0));
2236 // Backwards ranges are empty, never negative-length.
2237 assert_eq!(Value::range(5, 2, false).range_len(), Some(0));
2238 // i64 normalization: 1..=i32::MAX does not overflow.
2239 assert_eq!(
2240 Value::range(1, i32::MAX, true).range_end_exclusive(),
2241 Some(i64::from(i32::MAX) + 1)
2242 );
2243 assert_eq!(
2244 Value::range(i32::MIN, i32::MAX, true).range_len(),
2245 Some(1i64 << 32)
2246 );
2247 }
2248
2249 #[test]
2250 fn range_equality_is_content_equality() {
2251 // Same sequence, different written form: equal (F7 "content
2252 // equality" — the form is display fidelity, not content).
2253 assert_eq!(Value::range(1, 6, true), Value::range(1, 7, false));
2254 assert_eq!(Value::range(1, 7, false), Value::range(1, 6, true));
2255 // Same form, same bounds: equal.
2256 assert_eq!(Value::range(0, 3, false), Value::range(0, 3, false));
2257 // Different sequences: unequal.
2258 assert_ne!(Value::range(0, 3, false), Value::range(1, 3, false));
2259 assert_ne!(Value::range(0, 3, false), Value::range(0, 4, false));
2260 // Every empty range equals every other empty range (both denote
2261 // the zero-length sequence, like two empty arrays).
2262 assert_eq!(Value::range(0, 0, false), Value::range(5, 5, false));
2263 assert_eq!(Value::range(9, 2, false), Value::range(0, 0, false));
2264 // An empty range is never equal to a non-empty one.
2265 assert_ne!(Value::range(0, 0, false), Value::range(0, 1, false));
2266 // Cross-variant: a range is not an array, int, or anything else.
2267 assert_ne!(Value::range(0, 2, false), Value::array(vec![]));
2268 assert_ne!(Value::range(0, 2, false), Value::Int(0));
2269 }
2270
2271 #[test]
2272 fn range_serde_round_trip_preserves_the_written_form() {
2273 for v in [
2274 Value::range(1, 6, true),
2275 Value::range(0, 10, false),
2276 Value::range(-3, 3, false),
2277 Value::range(0, 0, false),
2278 Value::array(vec![Value::range(1, 2, true), Value::Int(9)]),
2279 ] {
2280 let back = json_round_trip(&v);
2281 assert_eq!(back, v);
2282 // The written form survives (not just content equality): the
2283 // triple round-trips bit-for-bit.
2284 if let Value::Range { .. } = &v {
2285 assert_eq!(back.as_range(), v.as_range());
2286 }
2287 }
2288 }
2289}