Skip to main content

sv_sim_sim/
objects.rs

1//! Classes plan (`docs/superpowers/plans/2026-10-03-classes.md`) Task 3: the
2//! object heap runtime (LRM 1800-2023 §8.29 memory management, §8.4 object
3//! identity). Pure-Rust, zero sv-lang/codegen dependency (this task's own
4//! acceptance text) — see `crate::netlist::ClassTemplateId`'s doc comment
5//! for why that id, despite being produced by the `frontend`-gated
6//! `netlist::classes` module, is itself defined unconditionally and usable
7//! from here.
8//!
9//! **Why a second table, not `collections.rs`'s.** [`collections`] stores
10//! SV *values*: `a = b` copies (copy-on-write via `Table::modify`), and an
11//! overwritten handle is unconditionally freed. A class object is a
12//! *reference* (§8.2: "the argument copied is the object handle, not the
13//! contents of the object") — `h2 = h1` makes both variables alias the SAME
14//! object, nothing is cloned, and an object is freed only when nothing
15//! references it any more. So this module is a structurally parallel
16//! generational handle table (same `version<<32 | index` shape, handle `0`
17//! reserved for `null`, a `Mutex`-guarded table with a free list so a reused
18//! slot never repeats a handle — directly modeled on `collections.rs`'s
19//! `Table`/`index`/`alloc`) but with real reference counting instead of
20//! copy-on-write.
21//!
22//! **Weak references fall out for free.** A stale `(version, index)` handle
23//! can never alias a different, later object at the same index: `alloc`
24//! always bumps that slot's `version` counter (never reused, only the index
25//! is recycled), and every read ([`field_get`], [`is_a`],
26//! [`dynamic_template`]) rejects a handle whose `version` does not match the
27//! table's current entry at that index. The LRM's "the clearing operation
28//! shall be performed atomically" (8.30.1) is satisfied by construction:
29//! the staleness check and the table's own `Mutex` are the same operation,
30//! so there is nothing to race.
31//!
32//! **Per-template metadata is a registry, not a call-site parameter.**
33//! [`release`]'s recursive free walk (which fields of a freed object are
34//! themselves class handles that must themselves be released) and [`is_a`]
35//! (which `ClassTemplateId`s a given dynamic template satisfies — self,
36//! every base, every flattened interface) both need information that is
37//! fixed per `ClassTemplateId`, computed once at elaboration time, and
38//! consumed by scattered call sites with no spare room to thread a `&[_]`/
39//! `&HashMap` argument through (most of those call sites are
40//! `#[no_mangle] extern "C"` ABI entry points reached from LLVM-emitted
41//! code, which can only pass scalar/pointer arguments it already has in
42//! hand — never a slice or map reference). So, exactly like
43//! [`collections`]'s own `table()` global, this module keeps one more
44//! process-wide `Mutex`-guarded table, [`registry`], populated once per
45//! template by [`register_handle_fields`]/[`register_ancestors`] (Task 5
46//! wires in the real `ObjectLayout`-derived field list; Task 10 wires in the
47//! real `netlist::classes::base_chain`/`flattened_interfaces`-derived
48//! ancestor set — this task's own tests call both directly with a hand-built
49//! list/set, exactly as the plan's Task 3 entry specifies). An
50//! unregistered template is simply treated as having no handle fields and
51//! no ancestors beyond itself — never a panic, since a template legitimately
52//! has nothing to register when, e.g., it declares no class-typed
53//! properties at all.
54
55use std::collections::{HashMap, HashSet};
56use std::sync::{Mutex, OnceLock};
57
58use crate::constraints::randc::RandcState;
59use crate::netlist::ClassTemplateId;
60
61/// One non-static field's location in an object's flat `fields` buffer:
62/// a word offset and a word count. `words` is always `1` for a field that
63/// holds a class handle (a handle is a single `u64`); wider counts are for
64/// ordinary (non-handle) SV data fields, which [`field_get`]/[`field_set`]
65/// handle identically — this module does not interpret field contents
66/// except where [`release`]'s recursive walk is told a slot is itself a
67/// handle (see [`register_handle_fields`]).
68#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
69pub struct FieldSlot {
70    pub offset: usize,
71    pub words: usize,
72}
73
74/// A bit window of one property: a leaf (a member of an unpacked struct, an
75/// element of a fixed-size unpacked array) addressed inside the property's one
76/// flat 4-state value (`state::storage`). The property holds `field_width` value
77/// bits then `field_width` unknown bits; the leaf is `width` bits at value bit
78/// `bit`, with its unknown bits at `field_width + bit`.
79#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
80pub struct Window {
81    /// The leaf's LSB within the property's value bits.
82    pub bit: u32,
83    /// The leaf's width in bits.
84    pub width: u32,
85    /// The whole property's width in bits (where its unknown half starts).
86    pub field_width: u32,
87}
88
89/// `n` (at most 64) bits of `words` starting at bit `lo`; bits past the end
90/// read 0.
91fn get_bits(words: &[u64], lo: usize, n: usize) -> u64 {
92    debug_assert!(n <= 64);
93    if n == 0 {
94        return 0;
95    }
96    let w = lo / 64;
97    let sh = lo % 64;
98    let a = words.get(w).copied().unwrap_or(0) >> sh;
99    let v = if sh == 0 {
100        a
101    } else {
102        a | (words.get(w + 1).copied().unwrap_or(0) << (64 - sh))
103    };
104    if n == 64 { v } else { v & ((1u64 << n) - 1) }
105}
106
107/// Overwrites `n` (at most 64) bits of `words` starting at bit `lo` with the low
108/// bits of `v`; bits past the end are dropped.
109fn set_bits(words: &mut [u64], lo: usize, n: usize, v: u64) {
110    debug_assert!(n <= 64);
111    if n == 0 {
112        return;
113    }
114    let mask = if n == 64 { u64::MAX } else { (1u64 << n) - 1 };
115    let v = v & mask;
116    let w = lo / 64;
117    let sh = lo % 64;
118    if let Some(x) = words.get_mut(w) {
119        *x = (*x & !(mask << sh)) | (v << sh);
120    }
121    if sh != 0
122        && sh + n > 64
123        && let Some(x) = words.get_mut(w + 1)
124    {
125        let hi_mask = mask >> (64 - sh);
126        *x = (*x & !hi_mask) | (v >> (64 - sh));
127    }
128}
129
130impl Window {
131    /// The window's value (`ceil(width/64)` words) out of the property words
132    /// `fields`, and its unknown bits in the same shape.
133    fn read(&self, fields: &[u64], val: &mut [u64], unk: &mut [u64]) {
134        for (i, (v, u)) in val.iter_mut().zip(unk.iter_mut()).enumerate() {
135            let lo = i * 64;
136            let n = (self.width as usize).saturating_sub(lo).min(64);
137            *v = get_bits(fields, self.bit as usize + lo, n);
138            *u = get_bits(fields, (self.field_width + self.bit) as usize + lo, n);
139        }
140    }
141
142    /// Stores `val` into the window of `fields` as a fully known value.
143    fn write(&self, fields: &mut [u64], val: &[u64]) {
144        for i in 0..(self.width as usize).div_ceil(64) {
145            let lo = i * 64;
146            let n = (self.width as usize - lo).min(64);
147            let v = val.get(i).copied().unwrap_or(0);
148            set_bits(fields, self.bit as usize + lo, n, v);
149            set_bits(fields, (self.field_width + self.bit) as usize + lo, n, 0);
150        }
151    }
152}
153
154/// A class-handle or collection-handle held INSIDE a larger property: a field
155/// or element of an unpacked struct, or an element of a fixed-size unpacked
156/// array, stored as part of the property's one flat value (LRM 1800-2023 7.2,
157/// 7.4: `state::storage`). The handle is a 64-bit value at bit `bit` of the
158/// property's value bits (the low half of the property's 4-state packed
159/// words, which is where [`field_get`] hands them out).
160#[derive(Clone, Copy, PartialEq, Eq, Debug)]
161pub struct NestedHandle {
162    /// The property holding the handle.
163    pub slot: FieldSlot,
164    /// The handle's LSB within the property's value bits.
165    pub bit: u32,
166    /// A collection handle (`crate::collections`) rather than an object's.
167    pub coll: bool,
168}
169
170/// The 64 value bits at `nh`'s position of `fields`; `None` when the position
171/// lies outside the buffer (a registration that does not fit the object).
172fn nested_get(fields: &[u64], nh: &NestedHandle) -> Option<u64> {
173    let w = nh.slot.offset + (nh.bit / 64) as usize;
174    let sh = nh.bit % 64;
175    let lo = *fields.get(w)? >> sh;
176    if sh == 0 {
177        return Some(lo);
178    }
179    Some(lo | (*fields.get(w + 1)? << (64 - sh)))
180}
181
182/// Overwrites the 64 value bits [`nested_get`] reads with `h`.
183fn nested_set(fields: &mut [u64], nh: &NestedHandle, h: u64) {
184    let w = nh.slot.offset + (nh.bit / 64) as usize;
185    let sh = nh.bit % 64;
186    if sh == 0 {
187        if let Some(d) = fields.get_mut(w) {
188            *d = h;
189        }
190        return;
191    }
192    if w + 1 >= fields.len() {
193        return;
194    }
195    let lo_mask = u64::MAX << sh;
196    fields[w] = (fields[w] & !lo_mask) | (h << sh);
197    fields[w + 1] = (fields[w + 1] & lo_mask) | (h >> (64 - sh));
198}
199
200/// Whether a `rand` property is plain `rand` or cyclic `randc` (LRM 18.4).
201#[derive(Clone, Copy, Debug, PartialEq, Eq)]
202pub enum RandKind {
203    Rand,
204    RandC,
205}
206
207/// One `rand`/`randc` property of a class template: where its value lives in
208/// an object's `fields` buffer and which kind it is. Fixed by the class
209/// declaration, so it lives on the template ([`register_rand_props`]), not on
210/// each object.
211#[derive(Clone, Copy, Debug, PartialEq, Eq)]
212pub struct RandProp {
213    pub slot: FieldSlot,
214    pub kind: RandKind,
215}
216
217/// Per-object `rand`/`randc` metadata (LRM §18.4, §18.8): the `rand_mode()`
218/// on/off state of each `rand`/`randc` property, in the order
219/// [`register_rand_props`] listed them (base-class properties first). Every
220/// object starts with all of them `true` -- LRM 18.8: "Initially, all
221/// `rand` variables are active". What is `rand` and what is `randc` is the
222/// template's ([`rand_props`]); this vector is the only mutable part, so the
223/// constraints plan's `randomize()` and `obj.x.rand_mode(0)` read/write it
224/// here without a second per-object side table.
225#[derive(Clone, Debug, Default, PartialEq, Eq)]
226pub struct RandMeta {
227    /// `rand_mode[i]` is whether the template's `i`th [`RandProp`] is active.
228    pub rand_mode: Vec<bool>,
229    /// Individual elements of a fixed-size array property switched off by
230    /// `obj.a[i].rand_mode(0)` (LRM 18.8: "random_variable can specify
231    /// individual elements using the corresponding index"), as `(index into
232    /// rand_mode, bit offset of the element within the property)` pairs. Empty
233    /// (the common case) costs nothing: a solve only rewrites its program when
234    /// one is present.
235    pub leaf_off: Vec<(usize, u32)>,
236    /// The converse of [`Self::leaf_off`]: elements switched back on alone
237    /// (`obj.a[i].rand_mode(1)`) while their array property is off, same pair
238    /// encoding. A pair here is a deviation from the property's mode, so it only
239    /// exists while that mode is off (LRM 18.8: a whole-property call sets every
240    /// element too, which clears both lists for it).
241    pub leaf_on: Vec<(usize, u32)>,
242}
243
244struct ObjEntry {
245    version: u32,
246    template: ClassTemplateId,
247    refcount: u32,
248    fields: Box<[u64]>,
249    prng_seed: u64,
250    rand_meta: RandMeta,
251    /// The `randc` cycle state of each `randc` field this object *owns*, keyed
252    /// by the field's word offset (LRM 18.4.2). An instance `randc` variable's
253    /// state lives in its object; a `static randc` variable's lives in the
254    /// class's static-property singleton object, so every instance shares it.
255    /// Absent until the variable is first drawn.
256    randc: HashMap<usize, RandcState>,
257    /// The word offsets of this object's `static` `rand`/`randc` properties
258    /// that `rand_mode(0)` switched off (LRM 18.8: a static variable's mode is
259    /// static, shared by every instance). Meaningful on a class's
260    /// static-property singleton object only; the rest of the program reads it
261    /// there ([`static_rand_mode`]).
262    static_rand_off: HashSet<usize>,
263    /// The ordinals of the constraint blocks `constraint_mode(0)` switched off
264    /// (LRM 18.9), in the numbering of the class's [`Program`] block table. On
265    /// an instance, its non-static blocks; on a static-property singleton, the
266    /// `static` blocks (whose mode is shared by every instance, 18.5.10).
267    ///
268    /// [`Program`]: crate::constraints::registry::Program
269    constraint_off: HashSet<u32>,
270}
271
272struct Table {
273    /// Index 0 is unused (handle 0 is the null object reference, §8.4).
274    entries: Vec<Option<ObjEntry>>,
275    /// Each slot's last version, so a reused slot never repeats a handle —
276    /// this is the one field that makes a stale weak handle detectable
277    /// (this module's own doc comment).
278    versions: Vec<u32>,
279    free: Vec<u32>,
280}
281
282fn table() -> &'static Mutex<Table> {
283    static T: OnceLock<Mutex<Table>> = OnceLock::new();
284    T.get_or_init(|| {
285        Mutex::new(Table {
286            entries: vec![None],
287            versions: vec![0],
288            free: Vec::new(),
289        })
290    })
291}
292
293/// Per-[`ClassTemplateId`] metadata [`release`]/[`is_a`] consult — see this
294/// module's doc comment on why this is a registry, not a call-site
295/// parameter.
296#[derive(Default, Clone)]
297struct TemplateInfo {
298    handle_fields: Vec<FieldSlot>,
299    /// The one-word fields that hold a queue / dynamic / associative array
300    /// handle ([`register_collection_fields`]): freed with the object and
301    /// cloned by [`copy`], through [`crate::collections`] outside this
302    /// module's lock (that module's lock order is collections then objects).
303    collection_fields: Vec<FieldSlot>,
304    /// The handles held inside struct / fixed-array properties, at a bit offset
305    /// of their property ([`register_nested_handles`]): released with the
306    /// object, cloned or retained by [`copy`].
307    nested_handles: Vec<NestedHandle>,
308    /// Self + every ancestor ([`is_a`]'s backing set) — always contains at
309    /// least the template's own id once registered; see
310    /// [`register_ancestors`].
311    ancestors: HashSet<ClassTemplateId>,
312    /// The template's `rand`/`randc` properties ([`register_rand_props`]).
313    rand_props: Vec<RandProp>,
314}
315
316fn registry() -> &'static Mutex<HashMap<ClassTemplateId, TemplateInfo>> {
317    static R: OnceLock<Mutex<HashMap<ClassTemplateId, TemplateInfo>>> = OnceLock::new();
318    R.get_or_init(|| Mutex::new(HashMap::new()))
319}
320
321/// Registers (idempotently — a later call replaces, never merges, an
322/// earlier one for the same template) the ordered list of `template`'s own
323/// non-static fields that are themselves class handles, consumed by
324/// [`release`]'s recursive free walk. Task 3 stub (this task's own tests
325/// call it directly with a hand-built list); Task 5 wires in the real
326/// `ObjectLayout`-derived list once that type exists.
327pub fn register_handle_fields(template: ClassTemplateId, slots: Vec<FieldSlot>) {
328    let mut r = registry().lock().unwrap_or_else(|e| e.into_inner());
329    r.entry(template).or_default().handle_fields = slots;
330}
331
332/// Registers (same discipline as [`register_handle_fields`]) the ordered
333/// list of `template`'s own non-static fields that hold a collection handle
334/// (a queue, dynamic array or associative array; ledger XC-059): [`release`]
335/// frees each such collection when its object dies and [`copy`] gives the new
336/// object its own copy-on-write clone (LRM 8.12: a shallow copy of a
337/// collection property is a value copy, never an alias).
338pub fn register_collection_fields(template: ClassTemplateId, slots: Vec<FieldSlot>) {
339    let mut r = registry().lock().unwrap_or_else(|e| e.into_inner());
340    r.entry(template).or_default().collection_fields = slots;
341}
342
343/// Registers (same discipline as [`register_handle_fields`]) the handles
344/// `template`'s struct and fixed-array properties hold at a bit offset of their
345/// value (ledger XC-059): a class handle or collection nested in an unpacked
346/// struct field or array element. [`release`] frees each when its object dies
347/// and [`copy`] retains each object / clones each collection, exactly as for
348/// a whole-property handle.
349pub fn register_nested_handles(template: ClassTemplateId, nested: Vec<NestedHandle>) {
350    let mut r = registry().lock().unwrap_or_else(|e| e.into_inner());
351    r.entry(template).or_default().nested_handles = nested;
352}
353
354/// Registers (idempotently, same discipline as
355/// [`register_handle_fields`]) the ancestor-closure set [`is_a`] consults:
356/// `template` itself plus every base class and every flattened interface.
357/// Task 3 stub (this task's own tests call it directly with a hand-built
358/// set); Task 10 wires in the real `netlist::classes::base_chain`/
359/// `flattened_interfaces`-derived computation.
360pub fn register_ancestors(template: ClassTemplateId, mut ancestors: HashSet<ClassTemplateId>) {
361    ancestors.insert(template);
362    let mut r = registry().lock().unwrap_or_else(|e| e.into_inner());
363    r.entry(template).or_default().ancestors = ancestors;
364}
365
366/// Registers (idempotently, same discipline as [`register_handle_fields`])
367/// `template`'s `rand`/`randc` properties, base-class properties first. Every
368/// object allocated afterwards starts with one active `rand_mode` bit per entry
369/// ([`RandMeta`]).
370pub fn register_rand_props(template: ClassTemplateId, props: Vec<RandProp>) {
371    let mut r = registry().lock().unwrap_or_else(|e| e.into_inner());
372    r.entry(template).or_default().rand_props = props;
373}
374
375/// One class template's registered runtime metadata as plain data: what a design
376/// image carries so a runner that never saw the class declarations can
377/// register the same templates ([`template_snapshot`], [`preload_templates`]).
378#[derive(Clone, Debug, PartialEq, Eq)]
379pub struct TemplateSnapshot {
380    pub id: ClassTemplateId,
381    pub handle_fields: Vec<FieldSlot>,
382    pub collection_fields: Vec<FieldSlot>,
383    pub nested_handles: Vec<NestedHandle>,
384    /// [`register_ancestors`]'s set, ascending by id (a set has no order of its
385    /// own; the image must encode deterministically).
386    pub ancestors: Vec<ClassTemplateId>,
387    pub rand_props: Vec<RandProp>,
388}
389
390/// The registered metadata of each template in `ids`, ascending by id and
391/// without repeats. A template nothing was registered for is skipped: it has no
392/// handle fields, no ancestors and no `rand` properties, which is what an
393/// absent entry already means.
394pub fn template_snapshot(ids: &[ClassTemplateId]) -> Vec<TemplateSnapshot> {
395    let mut ids = ids.to_vec();
396    ids.sort_by_key(|i| i.0);
397    ids.dedup();
398    let r = registry().lock().unwrap_or_else(|e| e.into_inner());
399    ids.into_iter()
400        .filter_map(|id| {
401            let info = r.get(&id)?;
402            let mut ancestors: Vec<ClassTemplateId> = info.ancestors.iter().copied().collect();
403            ancestors.sort_by_key(|i| i.0);
404            Some(TemplateSnapshot {
405                id,
406                handle_fields: info.handle_fields.clone(),
407                collection_fields: info.collection_fields.clone(),
408                nested_handles: info.nested_handles.clone(),
409                ancestors,
410                rand_props: info.rand_props.clone(),
411            })
412        })
413        .collect()
414}
415
416/// Registers every template of `snapshot` ([`template_snapshot`]'s inverse), each
417/// replacing what was registered for its id.
418pub fn preload_templates(snapshot: &[TemplateSnapshot]) {
419    let mut r = registry().lock().unwrap_or_else(|e| e.into_inner());
420    for t in snapshot {
421        let info = r.entry(t.id).or_default();
422        info.handle_fields = t.handle_fields.clone();
423        info.collection_fields = t.collection_fields.clone();
424        info.nested_handles = t.nested_handles.clone();
425        info.ancestors = t.ancestors.iter().copied().collect();
426        info.rand_props = t.rand_props.clone();
427    }
428}
429
430/// `template`'s registered `rand`/`randc` properties (empty for a template
431/// with none, or none registered).
432pub fn rand_props(template: ClassTemplateId) -> Vec<RandProp> {
433    registry()
434        .lock()
435        .unwrap_or_else(|e| e.into_inner())
436        .get(&template)
437        .map(|info| info.rand_props.clone())
438        .unwrap_or_default()
439}
440
441fn index(h: u64) -> usize {
442    (h & 0xffff_ffff) as usize
443}
444
445fn version_of(h: u64) -> u32 {
446    (h >> 32) as u32
447}
448
449impl Table {
450    fn alloc(&mut self, template: ClassTemplateId, field_words: usize) -> u64 {
451        let i = match self.free.pop() {
452            Some(i) => i as usize,
453            None => {
454                self.entries.push(None);
455                self.versions.push(0);
456                self.entries.len() - 1
457            }
458        };
459        let version = self.versions[i].wrapping_add(1);
460        self.versions[i] = version;
461        // Lock order table -> registry, the same as `release`.
462        let rand_mode = vec![
463            true;
464            registry()
465                .lock()
466                .unwrap_or_else(|e| e.into_inner())
467                .get(&template)
468                .map_or(0, |info| info.rand_props.len())
469        ];
470        self.entries[i] = Some(ObjEntry {
471            version,
472            template,
473            refcount: 1,
474            fields: vec![0u64; field_words].into(),
475            prng_seed: 0,
476            rand_meta: RandMeta {
477                rand_mode,
478                leaf_off: Vec::new(),
479                leaf_on: Vec::new(),
480            },
481            randc: HashMap::new(),
482            static_rand_off: HashSet::new(),
483            constraint_off: HashSet::new(),
484        });
485        (u64::from(version) << 32) | i as u64
486    }
487
488    /// The live entry `h` addresses, or `None` if `h` is the null handle, an
489    /// out-of-range index, a freed slot, or a stale version (the
490    /// weak-reference staleness check — this module's doc comment).
491    fn get(&self, h: u64) -> Option<&ObjEntry> {
492        let i = index(h);
493        if i == 0 {
494            return None;
495        }
496        let e = self.entries.get(i)?.as_ref()?;
497        (e.version == version_of(h)).then_some(e)
498    }
499
500    fn get_mut(&mut self, h: u64) -> Option<&mut ObjEntry> {
501        let i = index(h);
502        if i == 0 {
503            return None;
504        }
505        let v = version_of(h);
506        let e = self.entries.get_mut(i)?.as_mut()?;
507        (e.version == v).then_some(e)
508    }
509}
510
511/// Allocates a new object of `template` with `field_words` words of
512/// zero-initialized field storage, refcount 1. §1.1's target signature takes
513/// `layout: &ObjectLayout` instead of a bare word count — `ObjectLayout`
514/// does not exist until Task 5, so this task takes the one piece of it
515/// `alloc` itself actually needs; Task 5 adapts call sites to pass
516/// `layout.total_words()` (or equivalent) once that type exists, with no
517/// further change to this function's behavior.
518pub fn alloc(template: ClassTemplateId, field_words: usize) -> u64 {
519    ALLOCS_SINCE_GC.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
520    let h = table()
521        .lock()
522        .unwrap_or_else(|e| e.into_inner())
523        .alloc(template, field_words);
524    if let Some(hook) = CREATE_HOOK.get() {
525        hook(h);
526    }
527    h
528}
529
530/// Observers of object lifetimes (the VPI's `cbCreateObj`, `cbReclaimObj`,
531/// `cbEndOfObject`, IEEE 1800-2023 38.36.1): `create` hears every new object
532/// (its constructor has yet to run: the observer reports it later), and
533/// `reclaim` every object about to be freed, while it is still whole (no
534/// table lock is held, so it may read the object). Set once; until then
535/// alloc and release pay one atomic load.
536static CREATE_HOOK: std::sync::OnceLock<fn(u64)> = std::sync::OnceLock::new();
537static RECLAIM_HOOK: std::sync::OnceLock<fn(u64)> = std::sync::OnceLock::new();
538
539/// Installs the lifetime observers (see [`CREATE_HOOK`]); a second call is a
540/// no-op.
541pub fn set_lifecycle_hooks(create: fn(u64), reclaim: fn(u64)) {
542    let _ = CREATE_HOOK.set(create);
543    let _ = RECLAIM_HOOK.set(reclaim);
544}
545
546/// `+1` reference. A null (`0`) or already-stale handle is silently a
547/// no-op: `retain`/`release` are only ever called by codegen on a handle a
548/// variable/field/collection element already holds, and a null handle holds
549/// nothing to retain.
550pub fn retain(h: u64) {
551    let mut t = table().lock().unwrap_or_else(|e| e.into_inner());
552    if let Some(e) = t.get_mut(h) {
553        // Saturating: a count that reached `u32::MAX` stays there (the object
554        // is leaked, never freed early), rather than wrapping to 0.
555        e.refcount = e.refcount.saturating_add(1);
556    }
557}
558
559/// `-1` reference; at `0`, recursively [`release`]s every field
560/// [`register_handle_fields`] named as itself a class handle, then frees the
561/// slot (its index returns to the free list; its `version` counter is left
562/// alone, so the NEXT `alloc` at that index bumps it again and no future
563/// handle ever collides with this one — see this module's doc comment on
564/// weak-reference staleness). A null or already-stale handle is a no-op, the
565/// same as [`retain`].
566pub fn release(h: u64) {
567    // A worklist, not recursion: a long chain of objects (a linked list of
568    // a million nodes) is freed by one release of its head, and must not
569    // overflow the stack doing so.
570    let mut pending = vec![h];
571    let mut freed = false;
572    // Collection handles of the freed objects, released after the loop: a
573    // collection's drop releases the objects it holds, so freeing one while
574    // holding this table's lock would invert the lock order (collections
575    // first, then objects).
576    let mut colls: Vec<u64> = Vec::new();
577    while let Some(h) = pending.pop() {
578        {
579            let mut t = table().lock().unwrap_or_else(|e| e.into_inner());
580            let Some(e) = t.get_mut(h) else { continue };
581            e.refcount = e.refcount.saturating_sub(1);
582            if e.refcount > 0 {
583                continue;
584            }
585        }
586        // About to be reclaimed: an observer sees it whole, with no lock held.
587        if let Some(hook) = RECLAIM_HOOK.get() {
588            hook(h);
589        }
590        let mut t = table().lock().unwrap_or_else(|e| e.into_inner());
591        let Some(e) = t.get_mut(h) else { continue };
592        // The observer took a reference: the object lives on.
593        if e.refcount > 0 {
594            continue;
595        }
596        let template = e.template;
597        {
598            let r = registry().lock().unwrap_or_else(|e| e.into_inner());
599            if let Some(info) = r.get(&template) {
600                for slot in &info.handle_fields {
601                    if let Some(&w) = e.fields.get(slot.offset) {
602                        pending.push(w);
603                    }
604                }
605                for slot in &info.collection_fields {
606                    if let Some(&w) = e.fields.get(slot.offset) {
607                        colls.push(w);
608                    }
609                }
610                for nh in &info.nested_handles {
611                    if let Some(w) = nested_get(&e.fields, nh) {
612                        if nh.coll {
613                            colls.push(w);
614                        } else {
615                            pending.push(w);
616                        }
617                    }
618                }
619            }
620        }
621        let i = index(h);
622        t.entries[i] = None;
623        t.free.push(i as u32);
624        freed = true;
625    }
626    for c in colls {
627        crate::collections::release(c);
628    }
629    // A freed object is what makes a `weak_reference` to it read null (LRM
630    // 8.30.3: `wait (weak.get() == null)`); wake the waiters on object state.
631    // Every table lock is released here.
632    if freed {
633        crate::sched::notify_object_state_change();
634    }
635}
636
637/// LRM 1800-2023 §8.12 shallow copy (`new obj`): a new object (refcount 1,
638/// the caller's reference) of `h`'s DYNAMIC template -- not the handle's
639/// static type -- holding a word-for-word copy of its fields, each field that
640/// is itself a handle [`retain`]ed (the copy and the original now both name
641/// the nested object: it is shared, not duplicated). The constructor is not
642/// run. The copy's `randc` variables start a fresh cycle (a permutation in
643/// progress is the original's history, not part of its value). `None` for a null or stale `h` (§8.12 leaves a null source
644/// undefined; the caller decides).
645pub fn copy(h: u64) -> Option<u64> {
646    // Phase 1 (objects lock): what to copy. Collections are cloned in phase 2,
647    // outside the lock, so the lock order stays collections -> objects.
648    let (template, fields, rand_meta, constraint_off, prng_seed, colls, nested_hs) = {
649        let t = table().lock().unwrap_or_else(|e| e.into_inner());
650        let src = t.get(h)?;
651        let (colls, nested_hs): (Vec<usize>, Vec<NestedHandle>) = registry()
652            .lock()
653            .unwrap_or_else(|e| e.into_inner())
654            .get(&src.template)
655            .map(|info| {
656                (
657                    info.collection_fields.iter().map(|s| s.offset).collect(),
658                    info.nested_handles.clone(),
659                )
660            })
661            .unwrap_or_default();
662        (
663            src.template,
664            src.fields.clone(),
665            src.rand_meta.clone(),
666            src.constraint_off.clone(),
667            src.prng_seed,
668            colls,
669            nested_hs,
670        )
671    };
672    let mut fields = fields;
673    for off in colls {
674        if let Some(w) = fields.get_mut(off) {
675            *w = crate::collections::clone(*w);
676        }
677    }
678    // A collection inside a struct or array property is cloned the same way,
679    // written back at its bit offset; an object inside one is retained below.
680    let mut nested_objs: Vec<u64> = Vec::new();
681    for nh in &nested_hs {
682        let Some(w) = nested_get(&fields, nh) else {
683            continue;
684        };
685        if nh.coll {
686            nested_set(&mut fields, nh, crate::collections::clone(w));
687        } else {
688            nested_objs.push(w);
689        }
690    }
691    let mut t = table().lock().unwrap_or_else(|e| e.into_inner());
692    let mut nested: Vec<u64> = registry()
693        .lock()
694        .unwrap_or_else(|e| e.into_inner())
695        .get(&template)
696        .map(|info| {
697            info.handle_fields
698                .iter()
699                .filter_map(|s| fields.get(s.offset).copied())
700                .collect()
701        })
702        .unwrap_or_default();
703    nested.extend(nested_objs);
704    let new = t.alloc(template, fields.len());
705    for n in nested {
706        if let Some(e) = t.get_mut(n) {
707            e.refcount = e.refcount.saturating_add(1);
708        }
709    }
710    if let Some(e) = t.get_mut(new) {
711        e.fields = fields;
712        e.rand_meta = rand_meta;
713        e.constraint_off = constraint_off;
714        e.prng_seed = prng_seed;
715    }
716    drop(t);
717    // A shallow copy is a new object too (38.36.1 cbCreateObj).
718    if let Some(hook) = CREATE_HOOK.get() {
719        hook(new);
720    }
721    Some(new)
722}
723
724/// The number of objects currently alive (allocated and not yet freed by
725/// their last [`release`]) -- the observable of reference counting: a
726/// design that drops every handle it took must bring this back to where it
727/// started, and one that over-releases would already have printed a stale
728/// read, so a leak and a use-after-free are both visible from outside.
729pub fn live_count() -> usize {
730    table()
731        .lock()
732        .unwrap_or_else(|e| e.into_inner())
733        .entries
734        .iter()
735        .filter(|e| e.is_some())
736        .count()
737}
738
739/// `h`'s current reference count (`None` for a null or stale handle) --
740/// test-only observation of the retain/release discipline.
741pub fn refcount(h: u64) -> Option<u32> {
742    table()
743        .lock()
744        .unwrap_or_else(|e| e.into_inner())
745        .get(h)
746        .map(|e| e.refcount)
747}
748
749/// Reads field `slot` of the live object `h` into `out` (as many words as
750/// `slot.words`/`out` both have room for); `false` (leaving `out` alone) for
751/// a null, stale, or out-of-range handle/slot.
752pub fn field_get(h: u64, slot: FieldSlot, out: &mut [u64]) -> bool {
753    let t = table().lock().unwrap_or_else(|e| e.into_inner());
754    let Some(e) = t.get(h) else { return false };
755    if slot.offset + slot.words > e.fields.len() {
756        return false;
757    }
758    let n = out.len().min(slot.words);
759    out[..n].copy_from_slice(&e.fields[slot.offset..slot.offset + n]);
760    true
761}
762
763/// Writes field `slot` of the live object `h` from `val`; a null, stale, or
764/// out-of-range handle/slot is a no-op (no live storage exists to write) that
765/// returns `false`, so the caller can report the access (LRM 1800-2023 8.4: a
766/// member of a `null` handle is a run-time error). `true` when it wrote.
767pub fn field_set(h: u64, slot: FieldSlot, val: &[u64]) -> bool {
768    let changed = {
769        let mut t = table().lock().unwrap_or_else(|e| e.into_inner());
770        let Some(e) = t.get_mut(h) else { return false };
771        if slot.offset + slot.words > e.fields.len() {
772            return false;
773        }
774        let n = val.len().min(slot.words);
775        let dest = &mut e.fields[slot.offset..slot.offset + n];
776        let changed = dest != &val[..n];
777        dest.copy_from_slice(&val[..n]);
778        changed
779    };
780    // A `wait (obj.prop == v)` / `wait (weak.get() == null)` sleeps on object
781    // state, which no signal change reports. The table lock is released.
782    if changed {
783        crate::sched::notify_object_state_change();
784    }
785    true
786}
787
788/// Replaces the `mask` bits of the words at `offset` of the live object `h` with
789/// those of `val` (a staged nonblocking write of a class property, LRM
790/// 1800-2023 10.4.2); `false` for a dead handle or out-of-range span. Wakes
791/// `wait`ers when the property changed, like [`field_set`].
792pub fn field_merge(h: u64, offset: usize, val: &[u64], mask: &[u64]) -> bool {
793    let changed = {
794        let mut t = table().lock().unwrap_or_else(|e| e.into_inner());
795        let Some(e) = t.get_mut(h) else { return false };
796        let n = val.len().min(mask.len());
797        if offset + n > e.fields.len() {
798            return false;
799        }
800        let mut changed = false;
801        for i in 0..n {
802            let old = e.fields[offset + i];
803            let new = (old & !mask[i]) | (val[i] & mask[i]);
804            changed |= new != old;
805            e.fields[offset + i] = new;
806        }
807        changed
808    };
809    if changed {
810        crate::sched::notify_object_state_change();
811    }
812    true
813}
814
815/// Reads the leaf `win` of property `slot` of the live object `h`: its value
816/// into `val` and its unknown bits into `unk` (each `ceil(width/64)` words);
817/// `false` (leaving both alone) for a null, stale or out-of-range handle/slot.
818pub fn field_get_window(
819    h: u64,
820    slot: FieldSlot,
821    win: Window,
822    val: &mut [u64],
823    unk: &mut [u64],
824) -> bool {
825    let t = table().lock().unwrap_or_else(|e| e.into_inner());
826    let Some(e) = t.get(h) else { return false };
827    if slot.offset + slot.words > e.fields.len() {
828        return false;
829    }
830    win.read(&e.fields[slot.offset..slot.offset + slot.words], val, unk);
831    true
832}
833
834/// Writes the leaf `win` of property `slot` of the live object `h` as the known
835/// value `val`, leaving the rest of the property alone; like [`field_set`] it is
836/// a no-op returning `false` for a dead handle or bad slot, and it wakes
837/// `wait`ers when the property changed.
838pub fn field_set_window(h: u64, slot: FieldSlot, win: Window, val: &[u64]) -> bool {
839    let changed = {
840        let mut t = table().lock().unwrap_or_else(|e| e.into_inner());
841        let Some(e) = t.get_mut(h) else { return false };
842        if slot.offset + slot.words > e.fields.len() {
843            return false;
844        }
845        let dest = &mut e.fields[slot.offset..slot.offset + slot.words];
846        let before = dest.to_vec();
847        win.write(dest, val);
848        before != dest
849    };
850    if changed {
851        crate::sched::notify_object_state_change();
852    }
853    true
854}
855
856/// Whether `h` names a live object: not null, not freed, and not a stale
857/// handle to a slot a later object now occupies (the version check of this
858/// module's doc comment). This is the whole of `weak_reference#(T)::get()`
859/// (LRM 1800-2023 8.30.3): a weak reference stores the referent's bare handle
860/// bits without retaining it, and is "cleared" exactly when this turns false.
861pub fn is_live(h: u64) -> bool {
862    table()
863        .lock()
864        .unwrap_or_else(|e| e.into_inner())
865        .get(h)
866        .is_some()
867}
868
869/// The slot index of `h` -- `weak_reference#(T)::get_id`'s value (LRM
870/// 8.30.5): nonzero for every real object (index 0 is the null handle's),
871/// shared by every handle to one object whatever its static type, unique among
872/// the objects alive at once (one slot each), and reusable after the object is
873/// freed (which 8.30.5 explicitly allows). `0` for the null handle.
874pub fn slot_index(h: u64) -> u64 {
875    index(h) as u64
876}
877
878/// `h`'s dynamic (runtime) class — `None` for a null or stale handle.
879pub fn dynamic_template(h: u64) -> Option<ClassTemplateId> {
880    table()
881        .lock()
882        .unwrap_or_else(|e| e.into_inner())
883        .get(h)
884        .map(|e| e.template)
885}
886
887/// `$cast`'s runtime check (6.24.2/8.16): whether `h`'s dynamic class is
888/// `dest` or one of `dest`'s descendants/implementors in the sense that
889/// matters for a cast — concretely, whether `dest` is in `h`'s dynamic
890/// template's own registered ancestor-or-self closure
891/// ([`register_ancestors`]). A null/stale handle, or a dynamic template with
892/// nothing registered for it yet, is never `is_a` anything (including
893/// itself) — an unregistered template legitimately means "no class/interface
894/// hierarchy information is available yet", not "matches everything".
895pub fn is_a(h: u64, dest: ClassTemplateId) -> bool {
896    let Some(template) = dynamic_template(h) else {
897        return false;
898    };
899    template_is_a(template, dest)
900}
901
902/// [`is_a`] for a template rather than an object: whether `dest` is in
903/// `template`'s registered ancestor-or-self closure. A compile-time query (a
904/// call site asks which registered classes could be the dynamic class behind
905/// a static one).
906pub fn template_is_a(template: ClassTemplateId, dest: ClassTemplateId) -> bool {
907    registry()
908        .lock()
909        .unwrap_or_else(|e| e.into_inner())
910        .get(&template)
911        .is_some_and(|info| info.ancestors.contains(&dest))
912}
913
914/// `h`'s object-stability PRNG seed (§18.14.3) — `None` for a null/stale
915/// handle. [`alloc`] leaves it `0`; the runtime's `Alloc` op draws the real
916/// value from the creating process's RNG cell at `new`-time (LRM 18.14.2) and
917/// stores it here.
918pub fn prng_seed(h: u64) -> Option<u64> {
919    table()
920        .lock()
921        .unwrap_or_else(|e| e.into_inner())
922        .get(h)
923        .map(|e| e.prng_seed)
924}
925
926/// Sets `h`'s object-stability PRNG seed; a no-op on a null/stale handle.
927pub fn set_prng_seed(h: u64, seed: u64) {
928    let mut t = table().lock().unwrap_or_else(|e| e.into_inner());
929    if let Some(e) = t.get_mut(h) {
930        e.prng_seed = seed;
931    }
932}
933
934/// `h`'s `rand`/`randc` metadata (a clone — `None` for a null/stale handle).
935pub fn rand_meta(h: u64) -> Option<RandMeta> {
936    table()
937        .lock()
938        .unwrap_or_else(|e| e.into_inner())
939        .get(h)
940        .map(|e| e.rand_meta.clone())
941}
942
943/// Replaces `h`'s `rand`/`randc` metadata; a no-op on a null/stale handle.
944pub fn set_rand_meta(h: u64, meta: RandMeta) {
945    let mut t = table().lock().unwrap_or_else(|e| e.into_inner());
946    if let Some(e) = t.get_mut(h) {
947        e.rand_meta = meta;
948    }
949}
950
951/// Objects allocated since the last [`collect_cycles`] (see
952/// [`maybe_collect_cycles`]).
953static ALLOCS_SINCE_GC: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
954
955/// Frees every object that only other objects reference -- the reference
956/// cycles reference counting alone never frees (LRM 1800-2023 §8.29: an
957/// object no handle outside the heap can reach is reclaimed). Returns how
958/// many objects were freed.
959///
960/// Trial deletion over the whole heap: an object's references from other
961/// objects' handle fields are counted; one whose reference count exceeds
962/// that is held from outside (a variable, a process, a collection, a
963/// mailbox) and is a root; everything reachable from a root through handle
964/// fields is live, and the rest is garbage. A garbage object's references to
965/// live objects are dropped before it is freed, so every count stays exact.
966/// A reference held by a collection element counts as outside the heap, so
967/// a cycle through a collection is (conservatively) never freed.
968pub fn collect_cycles() -> usize {
969    let freed = collect_cycles_locked();
970    COLLECTED.fetch_add(freed, std::sync::atomic::Ordering::Relaxed);
971    freed
972}
973
974/// Every object [`collect_cycles`] has freed in this process.
975static COLLECTED: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
976
977/// How many objects [`collect_cycles`] has freed in this process.
978pub fn collected_count() -> usize {
979    COLLECTED.load(std::sync::atomic::Ordering::Relaxed)
980}
981
982fn collect_cycles_locked() -> usize {
983    ALLOCS_SINCE_GC.store(0, std::sync::atomic::Ordering::Relaxed);
984    let mut t = table().lock().unwrap_or_else(|e| e.into_inner());
985    let r = registry().lock().unwrap_or_else(|e| e.into_inner());
986    let handle_fields = |e: &ObjEntry| -> Vec<u64> {
987        r.get(&e.template)
988            .map(|info| {
989                info.handle_fields
990                    .iter()
991                    .filter_map(|s| e.fields.get(s.offset).copied())
992                    .collect()
993            })
994            .unwrap_or_default()
995    };
996    let live: Vec<u64> = t
997        .entries
998        .iter()
999        .enumerate()
1000        .filter_map(|(i, e)| e.as_ref().map(|e| (u64::from(e.version) << 32) | i as u64))
1001        .collect();
1002    let mut internal: HashMap<usize, u32> = HashMap::new();
1003    for &h in &live {
1004        let Some(e) = t.get(h) else { continue };
1005        for w in handle_fields(e) {
1006            if t.get(w).is_some() {
1007                *internal.entry(index(w)).or_default() += 1;
1008            }
1009        }
1010    }
1011    let mut marked: HashSet<usize> = HashSet::new();
1012    let mut stack: Vec<u64> = live
1013        .iter()
1014        .copied()
1015        .filter(|&h| {
1016            t.get(h)
1017                .is_some_and(|e| e.refcount > internal.get(&index(h)).copied().unwrap_or(0))
1018        })
1019        .collect();
1020    while let Some(h) = stack.pop() {
1021        if !marked.insert(index(h)) {
1022            continue;
1023        }
1024        if let Some(e) = t.get(h) {
1025            stack.extend(handle_fields(e).into_iter().filter(|&w| t.get(w).is_some()));
1026        }
1027    }
1028    let garbage: Vec<u64> = live
1029        .into_iter()
1030        .filter(|&h| !marked.contains(&index(h)))
1031        .collect();
1032    let mut drops: Vec<u64> = Vec::new();
1033    for &g in &garbage {
1034        if let Some(e) = t.get(g) {
1035            drops.extend(
1036                handle_fields(e)
1037                    .into_iter()
1038                    .filter(|w| marked.contains(&index(*w))),
1039            );
1040        }
1041    }
1042    for w in drops {
1043        if let Some(e) = t.get_mut(w) {
1044            e.refcount = e.refcount.saturating_sub(1);
1045        }
1046    }
1047    for &g in &garbage {
1048        let i = index(g);
1049        t.entries[i] = None;
1050        t.free.push(i as u32);
1051    }
1052    garbage.len()
1053}
1054
1055/// Runs [`collect_cycles`] once enough objects have been allocated since the
1056/// last collection to make it worthwhile (at least 4096, and at least as
1057/// many as are alive), so its cost stays proportional to allocation.
1058pub fn maybe_collect_cycles() -> usize {
1059    let since = ALLOCS_SINCE_GC.load(std::sync::atomic::Ordering::Relaxed);
1060    // Against what the last collection left alive, not today's count: that
1061    // includes the garbage allocated since, and would never be caught up.
1062    if since < 4096 || since < LIVE_AFTER_GC.load(std::sync::atomic::Ordering::Relaxed) {
1063        return 0;
1064    }
1065    let freed = collect_cycles();
1066    LIVE_AFTER_GC.store(live_count(), std::sync::atomic::Ordering::Relaxed);
1067    freed
1068}
1069
1070/// The live object count right after the last [`maybe_collect_cycles`]
1071/// collection.
1072static LIVE_AFTER_GC: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
1073
1074/// Whether the `rand`/`randc` property at word `offset` of `h` is active
1075/// (`rand_mode()`, LRM 18.8). `None` for a null/stale handle or an `offset` that
1076/// is no `rand` property of `h`'s class.
1077pub fn rand_mode_of(h: u64, offset: usize) -> Option<bool> {
1078    let t = table().lock().unwrap_or_else(|e| e.into_inner());
1079    let e = t.get(h)?;
1080    let i = rand_prop_index(e.template, offset)?;
1081    e.rand_meta.rand_mode.get(i).copied()
1082}
1083
1084/// Sets the `rand_mode()` of the `rand`/`randc` property at word `offset` of
1085/// `h`. `false` (changing nothing) for a null/stale handle or an `offset` that
1086/// is no `rand` property of `h`'s class.
1087pub fn set_rand_mode_of(h: u64, offset: usize, on: bool) -> bool {
1088    let mut t = table().lock().unwrap_or_else(|e| e.into_inner());
1089    let Some(e) = t.get_mut(h) else { return false };
1090    let Some(i) = rand_prop_index(e.template, offset) else {
1091        return false;
1092    };
1093    match e.rand_meta.rand_mode.get_mut(i) {
1094        Some(bit) => {
1095            *bit = on;
1096            // A whole-property call sets every element too (LRM 18.8).
1097            e.rand_meta.leaf_off.retain(|&(p, _)| p != i);
1098            e.rand_meta.leaf_on.retain(|&(p, _)| p != i);
1099            true
1100        }
1101        None => false,
1102    }
1103}
1104
1105/// `obj.v[i].rand_mode()` (LRM 18.8): whether the element at value bit `bit` of
1106/// the `rand` array property at word `offset` of `h` is active, i.e. its
1107/// property is and the element was not switched off alone. `None` as
1108/// [`rand_mode_of`].
1109pub fn leaf_rand_mode_of(h: u64, offset: usize, bit: u32) -> Option<bool> {
1110    let t = table().lock().unwrap_or_else(|e| e.into_inner());
1111    let e = t.get(h)?;
1112    let i = rand_prop_index(e.template, offset)?;
1113    let on = *e.rand_meta.rand_mode.get(i)?;
1114    let m = &e.rand_meta;
1115    Some(if on {
1116        !m.leaf_off.contains(&(i, bit))
1117    } else {
1118        m.leaf_on.contains(&(i, bit))
1119    })
1120}
1121
1122/// `obj.v[i].rand_mode(on)`: switches the one element at value bit `bit` of the
1123/// array property at word `offset` of `h`. `false` as [`set_rand_mode_of`].
1124pub fn set_leaf_rand_mode_of(h: u64, offset: usize, bit: u32, on: bool) -> bool {
1125    let mut t = table().lock().unwrap_or_else(|e| e.into_inner());
1126    let Some(e) = t.get_mut(h) else { return false };
1127    let Some(i) = rand_prop_index(e.template, offset) else {
1128        return false;
1129    };
1130    let m = &mut e.rand_meta;
1131    if i >= m.rand_mode.len() {
1132        return false;
1133    }
1134    m.leaf_off.retain(|&k| k != (i, bit));
1135    m.leaf_on.retain(|&k| k != (i, bit));
1136    // Only a deviation from the property's own mode is recorded.
1137    if m.rand_mode[i] && !on {
1138        m.leaf_off.push((i, bit));
1139    } else if !m.rand_mode[i] && on {
1140        m.leaf_on.push((i, bit));
1141    }
1142    true
1143}
1144
1145/// `obj.rand_mode(on)` with no variable (LRM 18.8): every `rand`/`randc`
1146/// property of `h` (its own, not those of the objects it holds). `false` for a
1147/// null/stale handle.
1148pub fn set_all_rand_modes(h: u64, on: bool) -> bool {
1149    let mut t = table().lock().unwrap_or_else(|e| e.into_inner());
1150    let Some(e) = t.get_mut(h) else { return false };
1151    e.rand_meta.rand_mode.iter_mut().for_each(|b| *b = on);
1152    e.rand_meta.leaf_off.clear();
1153    e.rand_meta.leaf_on.clear();
1154    true
1155}
1156
1157/// The index of the property at word `offset` among `template`'s `rand`
1158/// properties (the index into [`RandMeta::rand_mode`]).
1159fn rand_prop_index(template: ClassTemplateId, offset: usize) -> Option<usize> {
1160    registry()
1161        .lock()
1162        .unwrap_or_else(|e| e.into_inner())
1163        .get(&template)?
1164        .rand_props
1165        .iter()
1166        .position(|p| p.slot.offset == offset)
1167}
1168
1169/// Whether the `static` `rand` property at word `offset` of the static-property
1170/// object `h` is active (LRM 18.8: its mode is shared by every instance).
1171/// `None` for a null/stale handle.
1172pub fn static_rand_mode(h: u64, offset: usize) -> Option<bool> {
1173    table()
1174        .lock()
1175        .unwrap_or_else(|e| e.into_inner())
1176        .get(h)
1177        .map(|e| !e.static_rand_off.contains(&offset))
1178}
1179
1180/// Sets the mode of the `static` `rand` property at word `offset` of the
1181/// static-property object `h`; `false` for a null/stale handle.
1182pub fn set_static_rand_mode(h: u64, offset: usize, on: bool) -> bool {
1183    let mut t = table().lock().unwrap_or_else(|e| e.into_inner());
1184    let Some(e) = t.get_mut(h) else { return false };
1185    if on {
1186        e.static_rand_off.remove(&offset);
1187    } else {
1188        e.static_rand_off.insert(offset);
1189    }
1190    true
1191}
1192
1193/// Whether constraint block `ordinal` held by `h` (an instance's own block, or a
1194/// static-property object's `static` block) is active (LRM 18.9: all start
1195/// active). `None` for a null/stale handle.
1196pub fn constraint_mode_of(h: u64, ordinal: u32) -> Option<bool> {
1197    table()
1198        .lock()
1199        .unwrap_or_else(|e| e.into_inner())
1200        .get(h)
1201        .map(|e| !e.constraint_off.contains(&ordinal))
1202}
1203
1204/// The class-key -> static-singleton template map, filled by the compiler as
1205/// it hands out each class's static-property singleton id, so a run-time
1206/// observer (VPI) can find a class's singleton without the compiler's state.
1207fn static_singleton_keys() -> &'static Mutex<HashMap<String, ClassTemplateId>> {
1208    static K: OnceLock<Mutex<HashMap<String, ClassTemplateId>>> = OnceLock::new();
1209    K.get_or_init(|| Mutex::new(HashMap::new()))
1210}
1211
1212/// Records that class specialization `key` (`class_specialization_key`) keeps
1213/// its `static` properties and blocks in singleton objects of `template`.
1214pub fn register_static_singleton_key(key: &str, template: ClassTemplateId) {
1215    static_singleton_keys()
1216        .lock()
1217        .unwrap_or_else(|e| e.into_inner())
1218        .insert(key.to_owned(), template);
1219}
1220
1221/// Every registered `(class key, singleton template id)` pair, sorted by key:
1222/// what an AOT image carries so a precompiled run can answer
1223/// [`static_singleton_of`] without the compiler having run.
1224pub fn static_singleton_key_pairs() -> Vec<(String, u32)> {
1225    let mut v: Vec<(String, u32)> = static_singleton_keys()
1226        .lock()
1227        .unwrap_or_else(|e| e.into_inner())
1228        .iter()
1229        .map(|(k, t)| (k.clone(), t.0))
1230        .collect();
1231    v.sort();
1232    v
1233}
1234
1235/// The live static-property singleton of the class with specialization `key`:
1236/// `None` when the compiler never registered the key, `Some(None)` when no
1237/// singleton has been allocated yet (nothing has touched the class's statics,
1238/// so every static mode still has its default). LRM 18.5.10, 18.9.
1239pub fn static_singleton_of(key: &str) -> Option<Option<u64>> {
1240    let template = *static_singleton_keys()
1241        .lock()
1242        .unwrap_or_else(|e| e.into_inner())
1243        .get(key)?;
1244    let t = table().lock().unwrap_or_else(|e| e.into_inner());
1245    Some(t.entries.iter().enumerate().find_map(|(i, e)| {
1246        let e = e.as_ref()?;
1247        (e.template == template).then(|| (u64::from(e.version) << 32) | i as u64)
1248    }))
1249}
1250
1251/// Sets the mode of constraint block `ordinal` held by `h`; `false` for a
1252/// null/stale handle.
1253pub fn set_constraint_mode_of(h: u64, ordinal: u32, on: bool) -> bool {
1254    let mut t = table().lock().unwrap_or_else(|e| e.into_inner());
1255    let Some(e) = t.get_mut(h) else { return false };
1256    if on {
1257        e.constraint_off.remove(&ordinal);
1258    } else {
1259        e.constraint_off.insert(ordinal);
1260    }
1261    true
1262}
1263
1264/// The `randc` cycle state of the field at word `offset` of `h` (a clone; the
1265/// default, not-yet-drawn state if it has none). `None` for a null/stale
1266/// handle.
1267pub fn randc_state(h: u64, offset: usize) -> Option<RandcState> {
1268    table()
1269        .lock()
1270        .unwrap_or_else(|e| e.into_inner())
1271        .get(h)
1272        .map(|e| e.randc.get(&offset).cloned().unwrap_or_default())
1273}
1274
1275/// Stores `h`'s `randc` cycle states, `(word offset, state)` each; `false`
1276/// (storing nothing) for a null/stale handle.
1277pub fn set_randc_states(h: u64, states: Vec<(usize, RandcState)>) -> bool {
1278    let mut t = table().lock().unwrap_or_else(|e| e.into_inner());
1279    let Some(e) = t.get_mut(h) else { return false };
1280    e.randc.extend(states);
1281    true
1282}
1283
1284#[cfg(test)]
1285mod tests {
1286    use super::*;
1287
1288    // Every test below that touches the module-wide `registry()` (via
1289    // `register_handle_fields`/`register_ancestors`) or whose assertions
1290    // depend on exact handle/index values uses its own disjoint
1291    // `ClassTemplateId` range, never reused by another test in this file:
1292    // `table()`/`registry()` are process-wide statics shared by every test
1293    // in this module, and `cargo test` runs them concurrently by default.
1294    // Plain alloc/retain/release/field round-trips that only assert on
1295    // their OWN handle's contents are safe to share an id, since the
1296    // `Mutex` makes each individual table operation atomic and no test
1297    // reads another test's handle -- but nothing here does that, to keep
1298    // the independence obvious at a glance.
1299
1300    #[test]
1301    fn is_live_tracks_release_and_rejects_a_stale_handle_after_slot_reuse() {
1302        assert!(!is_live(0), "the null handle is never live");
1303        let a = alloc(ClassTemplateId(1100), 0);
1304        assert!(is_live(a));
1305        release(a);
1306        assert!(!is_live(a), "a freed handle is not live");
1307        // Whatever other test allocates into the freed slot, the old handle's
1308        // version no longer matches.
1309        let b = alloc(ClassTemplateId(1101), 0);
1310        assert!(is_live(b));
1311        assert!(!is_live(a), "a stale handle must not alias the newcomer");
1312        release(b);
1313    }
1314
1315    #[test]
1316    fn slot_index_is_nonzero_distinct_and_zero_for_null() {
1317        assert_eq!(slot_index(0), 0);
1318        let a = alloc(ClassTemplateId(1102), 0);
1319        let b = alloc(ClassTemplateId(1103), 0);
1320        assert_ne!(slot_index(a), 0);
1321        assert_ne!(slot_index(a), slot_index(b));
1322        release(a);
1323        release(b);
1324    }
1325
1326    #[test]
1327    fn alloc_field_set_field_get_round_trips() {
1328        let h = alloc(ClassTemplateId(1000), 2);
1329        let slot0 = FieldSlot {
1330            offset: 0,
1331            words: 1,
1332        };
1333        let slot1 = FieldSlot {
1334            offset: 1,
1335            words: 1,
1336        };
1337        field_set(h, slot0, &[42]);
1338        field_set(h, slot1, &[7]);
1339        let mut out = [0u64];
1340        assert!(field_get(h, slot0, &mut out));
1341        assert_eq!(out, [42]);
1342        assert!(field_get(h, slot1, &mut out));
1343        assert_eq!(out, [7]);
1344        assert_eq!(dynamic_template(h), Some(ClassTemplateId(1000)));
1345    }
1346
1347    #[test]
1348    fn two_retains_need_two_releases_before_the_slot_frees() {
1349        // A sentinel field value distinguishes "still a live slot with its
1350        // data intact" from "freed" (a freed slot's `field_get` fails
1351        // outright, never silently reading zeroed/reused memory).
1352        let h = alloc(ClassTemplateId(1001), 1);
1353        let slot = FieldSlot {
1354            offset: 0,
1355            words: 1,
1356        };
1357        field_set(h, slot, &[0xdead_beef]);
1358        retain(h); // refcount 2
1359        release(h); // refcount 1 -- still alive
1360        let mut out = [0u64];
1361        assert!(
1362            field_get(h, slot, &mut out),
1363            "one release of two retains must not free the slot"
1364        );
1365        assert_eq!(
1366            out,
1367            [0xdead_beef],
1368            "a leaked (still-referenced) slot's data is intact"
1369        );
1370        release(h); // refcount 0 -- now freed
1371        assert!(
1372            !field_get(h, slot, &mut out),
1373            "the second release must free the slot"
1374        );
1375    }
1376
1377    #[test]
1378    fn releasing_an_object_recursively_releases_a_handle_field_it_points() {
1379        let pointee_template = ClassTemplateId(1002);
1380        let pointer_template = ClassTemplateId(1003);
1381        let pointee = alloc(pointee_template, 1);
1382        let pointer = alloc(pointer_template, 1);
1383        let handle_slot = FieldSlot {
1384            offset: 0,
1385            words: 1,
1386        };
1387        field_set(pointer, handle_slot, &[pointee]);
1388        register_handle_fields(pointer_template, vec![handle_slot]);
1389
1390        retain(pointee); // the field store retained it: refcount 2 (alloc's 1 + this)
1391        assert_eq!(dynamic_template(pointee), Some(pointee_template));
1392
1393        release(pointer); // refcount 0 -- frees pointer, and recursively releases pointee once
1394        let mut out = [0u64];
1395        assert!(
1396            field_get(pointee, handle_slot, &mut out),
1397            "pointee still has one more reference after the recursive release"
1398        );
1399
1400        // Drop the remaining, direct reference: now it must actually free.
1401        release(pointee);
1402        assert!(!field_get(pointee, handle_slot, &mut out));
1403        assert_eq!(dynamic_template(pointee), None);
1404    }
1405
1406    /// The single most important test in this task (plan's own words):
1407    /// built against a freshly constructed, test-local [`Table`] rather
1408    /// than the module-wide [`table`] singleton, so the free-list reuse this
1409    /// test depends on ("the index just freed is the next one `alloc`
1410    /// hands out") is deterministic and never raced by another test's
1411    /// concurrent allocations against the same shared global.
1412    #[test]
1413    fn a_stale_handle_after_its_slot_is_recycled_fails_field_get() {
1414        // Mirrors `table()`'s own initialization (index 0 pre-reserved for
1415        // the null handle) rather than a bare `Table::default()`, so this
1416        // local table obeys the same index-0-is-never-allocated invariant
1417        // the module-wide singleton does.
1418        let mut t = Table {
1419            entries: vec![None],
1420            versions: vec![0],
1421            free: Vec::new(),
1422        };
1423        let h1 = t.alloc(ClassTemplateId(0), 1);
1424        t.get_mut(h1).unwrap().fields[0] = 1;
1425
1426        // Free h1's slot by hand (the public `release()` free-function also
1427        // does the recursive-children walk, which needs nothing here).
1428        let i = index(h1);
1429        t.entries[i] = None;
1430        t.free.push(i as u32);
1431
1432        let h2 = t.alloc(ClassTemplateId(1), 1);
1433        assert_eq!(
1434            index(h1),
1435            index(h2),
1436            "the only freed slot in a fresh table is always the next one `alloc` reuses"
1437        );
1438        assert_ne!(
1439            h1, h2,
1440            "alloc must bump the recycled index's version, or the stale handle would still match"
1441        );
1442        assert!(
1443            t.get(h1).is_none(),
1444            "a stale (old_version, index) handle must fail to resolve once its slot is recycled"
1445        );
1446        let e2 = t
1447            .get(h2)
1448            .expect("the new object at the recycled index is live");
1449        assert_eq!(
1450            e2.fields[0], 0,
1451            "the new object's fields start zeroed -- never carrying over the old occupant's data"
1452        );
1453    }
1454
1455    #[test]
1456    fn is_a_matches_self_and_registered_ancestors_only() {
1457        let base = ClassTemplateId(100);
1458        let derived = ClassTemplateId(101);
1459        let unrelated = ClassTemplateId(102);
1460        register_ancestors(derived, HashSet::from([base]));
1461        register_ancestors(base, HashSet::new());
1462
1463        let h = alloc(derived, 0);
1464        assert!(
1465            is_a(h, derived),
1466            "an object is always is_a its own dynamic class"
1467        );
1468        assert!(is_a(h, base), "a derived object is_a its registered base");
1469        assert!(
1470            !is_a(h, unrelated),
1471            "is_a must not match a template that was never registered as an ancestor"
1472        );
1473
1474        let base_h = alloc(base, 0);
1475        assert!(
1476            !is_a(base_h, derived),
1477            "is_a is one-directional: a base object is never is_a its derived class"
1478        );
1479    }
1480
1481    #[test]
1482    fn retain_release_and_field_access_on_the_null_handle_are_safe_no_ops() {
1483        assert_eq!(dynamic_template(0), None);
1484        assert!(!is_a(0, ClassTemplateId(1004)));
1485        retain(0);
1486        release(0);
1487        let slot = FieldSlot {
1488            offset: 0,
1489            words: 1,
1490        };
1491        let mut out = [0u64];
1492        assert!(!field_get(0, slot, &mut out));
1493        field_set(0, slot, &[1]); // must not panic
1494    }
1495
1496    #[test]
1497    fn prng_seed_and_rand_meta_round_trip_and_are_none_once_freed() {
1498        let h = alloc(ClassTemplateId(1005), 0);
1499        assert_eq!(
1500            prng_seed(h),
1501            Some(0),
1502            "freshly allocated, before any seeding hook runs"
1503        );
1504        set_prng_seed(h, 0x1234_5678);
1505        assert_eq!(prng_seed(h), Some(0x1234_5678));
1506
1507        assert_eq!(
1508            rand_meta(h),
1509            Some(RandMeta::default()),
1510            "a template with no registered rand properties has no rand_mode bits"
1511        );
1512        set_rand_meta(
1513            h,
1514            RandMeta {
1515                rand_mode: vec![true, false],
1516                ..Default::default()
1517            },
1518        );
1519        assert_eq!(
1520            rand_meta(h),
1521            Some(RandMeta {
1522                rand_mode: vec![true, false],
1523                ..Default::default()
1524            })
1525        );
1526
1527        release(h);
1528        assert_eq!(prng_seed(h), None);
1529        assert_eq!(rand_meta(h), None);
1530    }
1531
1532    #[test]
1533    fn rand_props_are_template_level_and_every_object_starts_all_active() {
1534        let t = ClassTemplateId(1040);
1535        let a = RandProp {
1536            slot: FieldSlot {
1537                offset: 0,
1538                words: 1,
1539            },
1540            kind: RandKind::Rand,
1541        };
1542        let b = RandProp {
1543            slot: FieldSlot {
1544                offset: 2,
1545                words: 2,
1546            },
1547            kind: RandKind::RandC,
1548        };
1549        assert_eq!(rand_props(t), Vec::new(), "nothing registered yet");
1550        register_rand_props(t, vec![a, b]);
1551        assert_eq!(rand_props(t), vec![a, b]);
1552        let h1 = alloc(t, 4);
1553        let h2 = alloc(t, 4);
1554        let all_on = RandMeta {
1555            rand_mode: vec![true, true],
1556            ..Default::default()
1557        };
1558        assert_eq!(rand_meta(h1), Some(all_on.clone()));
1559        // The mutable part is per object: turning one property off on `h1`
1560        // does not touch `h2`, and a shallow copy carries it along.
1561        set_rand_meta(
1562            h1,
1563            RandMeta {
1564                rand_mode: vec![true, false],
1565                ..Default::default()
1566            },
1567        );
1568        assert_eq!(rand_meta(h2), Some(all_on));
1569        let dup = copy(h1).unwrap();
1570        assert_eq!(rand_meta(dup).unwrap().rand_mode, vec![true, false]);
1571        // Re-registering replaces, never merges.
1572        register_rand_props(t, vec![a]);
1573        assert_eq!(rand_props(t), vec![a]);
1574        let h3 = alloc(t, 4);
1575        assert_eq!(rand_meta(h3).unwrap().rand_mode, vec![true]);
1576        for h in [h1, h2, h3, dup] {
1577            release(h);
1578        }
1579    }
1580
1581    fn one_word(offset: usize) -> FieldSlot {
1582        FieldSlot { offset, words: 1 }
1583    }
1584
1585    #[test]
1586    fn refcount_tracks_retain_and_release_and_is_none_when_dead() {
1587        let h = alloc(ClassTemplateId(1006), 1);
1588        assert_eq!(refcount(h), Some(1));
1589        retain(h);
1590        assert_eq!(refcount(h), Some(2));
1591        release(h);
1592        assert_eq!(refcount(h), Some(1));
1593        release(h);
1594        assert_eq!(refcount(h), None, "a freed handle has no count");
1595        assert_eq!(refcount(0), None, "null has no count");
1596    }
1597
1598    #[test]
1599    fn live_count_sees_an_allocation_until_its_last_release() {
1600        // Process-global and other tests allocate concurrently, so only the
1601        // direction is asserted: with this object alive the count is at
1602        // least 1, and `refcount` (per handle, race-free) shows it freed.
1603        let h = alloc(ClassTemplateId(1007), 1);
1604        assert!(live_count() >= 1);
1605        release(h);
1606        assert_eq!(refcount(h), None);
1607    }
1608
1609    #[test]
1610    fn copy_is_shallow_of_the_dynamic_class_and_retains_nested_handles() {
1611        // LRM 8.12: properties are copied (a handle copied, not the object it
1612        // names) into a NEW object of the source's dynamic class.
1613        let inner_t = ClassTemplateId(1008);
1614        let outer_t = ClassTemplateId(1009);
1615        register_handle_fields(outer_t, vec![one_word(1)]);
1616        let inner = alloc(inner_t, 1);
1617        field_set(inner, one_word(0), &[5]);
1618        let src = alloc(outer_t, 2);
1619        field_set(src, one_word(0), &[77]);
1620        field_set(src, one_word(1), &[inner]); // src's reference to inner: the alloc's 1
1621        assert_eq!(refcount(inner), Some(1));
1622
1623        let dup = copy(src).expect("a live source copies");
1624        assert_ne!(dup, src, "a copy is a distinct object");
1625        assert_eq!(dynamic_template(dup), Some(outer_t));
1626        assert_eq!(refcount(dup), Some(1), "the caller owns the copy");
1627        assert_eq!(refcount(src), Some(1), "the source is untouched");
1628        assert_eq!(
1629            refcount(inner),
1630            Some(2),
1631            "the nested object is shared: both owners hold a reference"
1632        );
1633        let mut out = [0u64];
1634        assert!(field_get(dup, one_word(0), &mut out));
1635        assert_eq!(out, [77], "scalar properties are copied by value");
1636        assert!(field_get(dup, one_word(1), &mut out));
1637        assert_eq!(out, [inner], "the handle is copied, not the object");
1638
1639        // The copy is independent of the original from here on.
1640        field_set(dup, one_word(0), &[88]);
1641        assert!(field_get(src, one_word(0), &mut out));
1642        assert_eq!(out, [77]);
1643
1644        // Freeing the original leaves the copy's reference to the nested
1645        // object; freeing the copy then frees it.
1646        release(src);
1647        assert_eq!(refcount(inner), Some(1));
1648        release(dup);
1649        assert_eq!(refcount(inner), None);
1650    }
1651
1652    #[test]
1653    fn copy_keeps_the_prng_seed_and_rand_meta() {
1654        let src = alloc(ClassTemplateId(1010), 0);
1655        set_prng_seed(src, 99);
1656        set_rand_meta(
1657            src,
1658            RandMeta {
1659                rand_mode: vec![false, true],
1660                ..Default::default()
1661            },
1662        );
1663        let dup = copy(src).unwrap();
1664        assert_eq!(prng_seed(dup), Some(99));
1665        assert_eq!(
1666            rand_meta(dup),
1667            Some(RandMeta {
1668                rand_mode: vec![false, true],
1669                ..Default::default()
1670            })
1671        );
1672        release(src);
1673        release(dup);
1674    }
1675
1676    /// LRM 18.8: one array element switched off alone reads back off, leaves its
1677    /// neighbours and the property on, and a whole-property call sets it again.
1678    #[test]
1679    fn leaf_rand_mode_is_per_element_and_reset_by_the_whole_property() {
1680        let t = ClassTemplateId(1031);
1681        register_rand_props(
1682            t,
1683            vec![RandProp {
1684                slot: one_word(0),
1685                kind: RandKind::Rand,
1686            }],
1687        );
1688        let h = alloc(t, 2);
1689        assert_eq!(leaf_rand_mode_of(h, 0, 8), Some(true));
1690        assert!(set_leaf_rand_mode_of(h, 0, 8, false));
1691        assert_eq!(leaf_rand_mode_of(h, 0, 8), Some(false));
1692        assert_eq!(leaf_rand_mode_of(h, 0, 0), Some(true));
1693        assert_eq!(rand_mode_of(h, 0), Some(true));
1694        assert!(
1695            !set_leaf_rand_mode_of(h, 1, 8, false),
1696            "offset 1 is no rand property"
1697        );
1698        assert!(set_rand_mode_of(h, 0, true));
1699        assert_eq!(leaf_rand_mode_of(h, 0, 8), Some(true));
1700        assert!(set_leaf_rand_mode_of(h, 0, 8, false));
1701        assert!(set_all_rand_modes(h, true));
1702        assert_eq!(leaf_rand_mode_of(h, 0, 8), Some(true));
1703        // LRM 18.8: with the whole property off, one element can be switched back on alone.
1704        assert!(set_rand_mode_of(h, 0, false));
1705        assert_eq!(leaf_rand_mode_of(h, 0, 8), Some(false));
1706        assert!(set_leaf_rand_mode_of(h, 0, 8, true));
1707        assert_eq!(leaf_rand_mode_of(h, 0, 8), Some(true));
1708        assert_eq!(leaf_rand_mode_of(h, 0, 0), Some(false));
1709        assert!(set_leaf_rand_mode_of(h, 0, 8, false));
1710        assert_eq!(leaf_rand_mode_of(h, 0, 8), Some(false));
1711        assert!(set_leaf_rand_mode_of(h, 0, 8, true));
1712        assert!(set_rand_mode_of(h, 0, false));
1713        assert_eq!(leaf_rand_mode_of(h, 0, 8), Some(false), "whole call resets");
1714        release(h);
1715    }
1716
1717    /// C20 (LRM 18.8): `rand_mode()` is addressed by the property's word
1718    /// offset, switches one property and no other, and refuses an offset that
1719    /// is no `rand` property rather than flipping some other bit.
1720    #[test]
1721    fn rand_mode_is_addressed_by_property_offset() {
1722        let t = ClassTemplateId(1030);
1723        register_rand_props(
1724            t,
1725            vec![
1726                RandProp {
1727                    slot: one_word(0),
1728                    kind: RandKind::Rand,
1729                },
1730                RandProp {
1731                    slot: one_word(3),
1732                    kind: RandKind::RandC,
1733                },
1734            ],
1735        );
1736        let h = alloc(t, 4);
1737        assert_eq!(rand_mode_of(h, 0), Some(true));
1738        assert_eq!(rand_mode_of(h, 3), Some(true));
1739        assert!(set_rand_mode_of(h, 3, false));
1740        assert_eq!(rand_mode_of(h, 0), Some(true));
1741        assert_eq!(rand_mode_of(h, 3), Some(false));
1742        // offset 1 is a plain field: no bit to read or flip
1743        assert_eq!(rand_mode_of(h, 1), None);
1744        assert!(!set_rand_mode_of(h, 1, false));
1745        assert_eq!(rand_meta(h).unwrap().rand_mode, vec![true, false]);
1746        assert!(set_all_rand_modes(h, false));
1747        assert_eq!(rand_meta(h).unwrap().rand_mode, vec![false, false]);
1748        assert!(set_all_rand_modes(h, true));
1749        assert_eq!(rand_meta(h).unwrap().rand_mode, vec![true, true]);
1750        release(h);
1751        // a freed handle is dead for every form
1752        assert_eq!(rand_mode_of(h, 0), None);
1753        assert!(!set_rand_mode_of(h, 0, true));
1754        assert!(!set_all_rand_modes(h, true));
1755    }
1756
1757    /// C20 (LRM 18.8, 18.9): a static property's mode and a constraint
1758    /// block's mode live on the object that holds them, start on, and survive
1759    /// a shallow copy only where the LRM says an object's own state does.
1760    #[test]
1761    fn static_rand_and_constraint_modes_are_kept_per_holding_object() {
1762        let t = ClassTemplateId(1031);
1763        let h = alloc(t, 2);
1764        assert_eq!(static_rand_mode(h, 1), Some(true));
1765        assert_eq!(constraint_mode_of(h, 2), Some(true));
1766        assert!(set_static_rand_mode(h, 1, false));
1767        assert!(set_constraint_mode_of(h, 2, false));
1768        assert_eq!(static_rand_mode(h, 1), Some(false));
1769        assert_eq!(static_rand_mode(h, 0), Some(true));
1770        assert_eq!(constraint_mode_of(h, 2), Some(false));
1771        assert_eq!(constraint_mode_of(h, 1), Some(true));
1772        let dup = copy(h).unwrap();
1773        assert_eq!(constraint_mode_of(dup, 2), Some(false));
1774        assert!(set_constraint_mode_of(h, 2, true));
1775        assert_eq!(constraint_mode_of(h, 2), Some(true));
1776        assert_eq!(constraint_mode_of(dup, 2), Some(false));
1777        assert!(set_static_rand_mode(h, 1, true));
1778        assert_eq!(static_rand_mode(h, 1), Some(true));
1779        release(h);
1780        release(dup);
1781        assert_eq!(static_rand_mode(h, 1), None);
1782        assert_eq!(constraint_mode_of(h, 2), None);
1783        assert!(!set_static_rand_mode(h, 1, false));
1784        assert!(!set_constraint_mode_of(h, 2, false));
1785    }
1786
1787    /// XC-059: a collection property is freed with its object and cloned (not
1788    /// aliased) by a shallow copy.
1789    #[test]
1790    fn a_collection_field_is_freed_with_its_object_and_cloned_by_copy() {
1791        use crate::collections::{self, KeyKind, Shape};
1792        let shape = Shape {
1793            elem_words: 1,
1794            assoc: false,
1795            key: KeyKind::Int {
1796                width: 32,
1797                signed: true,
1798            },
1799            obj: false,
1800        };
1801        let t = ClassTemplateId(1060);
1802        register_collection_fields(t, vec![one_word(1)]);
1803        let src = alloc(t, 2);
1804        let q = collections::set(&shape, 0, 0, &[7], true);
1805        let q = collections::set(&shape, q, 1, &[8], true);
1806        assert_eq!(collections::size(q), 2);
1807        field_set(src, one_word(1), &[q]);
1808
1809        let dup = copy(src).expect("copies");
1810        let mut out = [0u64];
1811        assert!(field_get(dup, one_word(1), &mut out));
1812        let q2 = out[0];
1813        assert_ne!(q2, q, "the copy has its own collection handle");
1814        assert_eq!(collections::size(q2), 2);
1815        // Changing the copy's collection leaves the original's alone.
1816        let q2b = collections::set(&shape, q2, 0, &[99], true);
1817        let mut e = [0u64];
1818        assert!(collections::get(q, 0, &mut e));
1819        assert_eq!(e, [7]);
1820        assert!(collections::get(q2b, 0, &mut e));
1821        assert_eq!(e, [99]);
1822        field_set(dup, one_word(1), &[q2b]);
1823
1824        release(src);
1825        assert!(!collections::is_live(q), "freed with the owning object");
1826        assert_eq!(collections::size(q2b), 2, "the copy's collection survives");
1827        release(dup);
1828        assert!(!collections::is_live(q2b));
1829    }
1830
1831    /// XC-059: a handle held at a (word-unaligned) bit offset of a struct or
1832    /// array property is released with the object, retained by a copy, and a
1833    /// nested collection is cloned and written back at its bit offset.
1834    #[test]
1835    fn nested_handles_at_unaligned_bit_offsets_follow_the_object() {
1836        use crate::collections::{self, KeyKind, Shape};
1837        let shape = Shape {
1838            elem_words: 1,
1839            assoc: false,
1840            key: KeyKind::Int {
1841                width: 32,
1842                signed: true,
1843            },
1844            obj: false,
1845        };
1846        // One 5-word property (value bits 0..160 in words 0-2, unk above): an
1847        // object handle at bit 8 and a collection handle at bit 72.
1848        let t = ClassTemplateId(1021);
1849        let slot = FieldSlot {
1850            offset: 0,
1851            words: 5,
1852        };
1853        let obj_at = NestedHandle {
1854            slot,
1855            bit: 8,
1856            coll: false,
1857        };
1858        let coll_at = NestedHandle {
1859            slot,
1860            bit: 72,
1861            coll: true,
1862        };
1863        register_nested_handles(t, vec![obj_at, coll_at]);
1864        let leaf = alloc(ClassTemplateId(1022), 1);
1865        let q = collections::set(&shape, 0, 0, &[5], true);
1866        let src = alloc(t, 5);
1867        let mut fields = vec![0u64; 5];
1868        // Write through the same helpers the heap reads with.
1869        nested_set(&mut fields, &obj_at, leaf);
1870        nested_set(&mut fields, &coll_at, q);
1871        assert_eq!(nested_get(&fields, &obj_at), Some(leaf));
1872        assert_eq!(nested_get(&fields, &coll_at), Some(q), "no overlap clobber");
1873        assert!(field_set(src, slot, &fields));
1874
1875        let dup = copy(src).expect("copies");
1876        assert_eq!(
1877            refcount(leaf),
1878            Some(2),
1879            "the copy retains the nested object"
1880        );
1881        let mut out = vec![0u64; 5];
1882        assert!(field_get(dup, slot, &mut out));
1883        let q2 = nested_get(&out, &coll_at).expect("in range");
1884        assert_ne!(q2, q, "the copy owns a clone of the nested collection");
1885        assert_eq!(collections::size(q2), 1);
1886        assert_eq!(nested_get(&out, &obj_at), Some(leaf));
1887
1888        release(src);
1889        assert_eq!(
1890            refcount(leaf),
1891            Some(1),
1892            "the original dropped its reference"
1893        );
1894        assert!(
1895            !collections::is_live(q),
1896            "the nested collection died with it"
1897        );
1898        release(dup);
1899        assert_eq!(refcount(leaf), None, "the last holder freed it");
1900        assert!(!collections::is_live(q2));
1901    }
1902
1903    /// XC-059: a leaf window reads and writes only its own value and unknown
1904    /// bits of the property, across word boundaries, and a write makes the
1905    /// leaf known without touching its neighbours.
1906    #[test]
1907    fn windows_read_and_write_only_their_own_bits() {
1908        // A 100-bit property: 100 value bits then 100 unknown bits (4 words).
1909        let slot = FieldSlot {
1910            offset: 1,
1911            words: 4,
1912        };
1913        let h = alloc(ClassTemplateId(1031), 5);
1914        // Everything unknown and set: val bits all 1, unk bits all 1.
1915        assert!(field_set(h, slot, &[u64::MAX; 4]));
1916        // A 70-bit leaf at bit 5 (straddles two value words and the unk half).
1917        let wide = Window {
1918            bit: 5,
1919            width: 70,
1920            field_width: 100,
1921        };
1922        let mut val = [0u64; 2];
1923        let mut unk = [0u64; 2];
1924        assert!(field_get_window(h, slot, wide, &mut val, &mut unk));
1925        assert_eq!(val, [u64::MAX, 0x3f]);
1926        assert_eq!(unk, [u64::MAX, 0x3f]);
1927
1928        assert!(field_set_window(
1929            h,
1930            slot,
1931            wide,
1932            &[0x0123_4567_89ab_cdef, 0x15]
1933        ));
1934        assert!(field_get_window(h, slot, wide, &mut val, &mut unk));
1935        assert_eq!(val, [0x0123_4567_89ab_cdef, 0x15]);
1936        assert_eq!(unk, [0, 0], "a write makes the leaf known");
1937
1938        // The neighbours below (bits 0..5) and above (75..100) are untouched.
1939        let low = Window {
1940            bit: 0,
1941            width: 5,
1942            field_width: 100,
1943        };
1944        let high = Window {
1945            bit: 75,
1946            width: 25,
1947            field_width: 100,
1948        };
1949        let (mut v, mut u) = ([0u64; 1], [0u64; 1]);
1950        assert!(field_get_window(h, slot, low, &mut v, &mut u));
1951        assert_eq!((v[0], u[0]), (0x1f, 0x1f));
1952        assert!(field_get_window(h, slot, high, &mut v, &mut u));
1953        assert_eq!((v[0], u[0]), ((1 << 25) - 1, (1 << 25) - 1));
1954
1955        // A narrow aligned leaf, and an unchanged write does not disturb it.
1956        let one = Window {
1957            bit: 64,
1958            width: 8,
1959            field_width: 100,
1960        };
1961        assert!(field_set_window(h, slot, one, &[0xa5]));
1962        assert!(field_get_window(h, slot, one, &mut v, &mut u));
1963        assert_eq!((v[0], u[0]), (0xa5, 0));
1964
1965        // A dead handle is reported, not written.
1966        release(h);
1967        assert!(!field_set_window(h, slot, one, &[1]));
1968        assert!(!field_get_window(h, slot, one, &mut v, &mut u));
1969    }
1970
1971    #[test]
1972    fn copy_of_null_or_a_stale_handle_is_none() {
1973        assert_eq!(copy(0), None);
1974        let h = alloc(ClassTemplateId(1011), 1);
1975        release(h);
1976        assert_eq!(copy(h), None);
1977    }
1978
1979    #[test]
1980    fn releasing_the_head_of_a_long_chain_frees_it_without_recursion() {
1981        // 100k nodes, each owning the next through a registered handle field:
1982        // a recursive free would overflow the test thread's stack.
1983        let t = ClassTemplateId(1012);
1984        register_handle_fields(t, vec![one_word(0)]);
1985        let head = alloc(t, 1);
1986        let mut prev = head;
1987        let mut all = vec![head];
1988        for _ in 0..100_000 {
1989            let next = alloc(t, 1);
1990            field_set(prev, one_word(0), &[next]);
1991            all.push(next);
1992            prev = next;
1993        }
1994        release(head);
1995        assert!(all.iter().all(|&h| refcount(h).is_none()));
1996    }
1997
1998    /// Two objects pointing at each other, and a third one they both point
1999    /// at that something outside still holds: dropping the outside handles
2000    /// to the pair leaks them under reference counting alone; the cycle
2001    /// collector frees exactly the pair, and leaves the shared object with
2002    /// only the outside reference.
2003    #[test]
2004    fn a_dropped_cycle_is_collected_and_what_it_shared_survives() {
2005        let t = ClassTemplateId(1900);
2006        let next = FieldSlot {
2007            offset: 0,
2008            words: 1,
2009        };
2010        let other = FieldSlot {
2011            offset: 1,
2012            words: 1,
2013        };
2014        register_handle_fields(t, vec![next, other]);
2015        let a = alloc(t, 2);
2016        let b = alloc(t, 2);
2017        let shared = alloc(t, 2);
2018        // a.next = b; b.next = a; a.other = b.other = shared.
2019        field_set(a, next, &[b]);
2020        retain(b);
2021        field_set(b, next, &[a]);
2022        retain(a);
2023        field_set(a, other, &[shared]);
2024        retain(shared);
2025        field_set(b, other, &[shared]);
2026        retain(shared);
2027        // The outside drops a and b.
2028        release(a);
2029        release(b);
2030        assert_eq!(refcount(a), Some(1), "reference counting alone leaks");
2031        assert_eq!(refcount(shared), Some(3));
2032        // A cycle still held from outside is not garbage.
2033        let c = alloc(t, 2);
2034        let d = alloc(t, 2);
2035        field_set(c, next, &[d]);
2036        retain(d);
2037        field_set(d, next, &[c]);
2038        retain(c);
2039        release(d);
2040        assert!(collect_cycles() >= 2);
2041        assert_eq!(refcount(a), None);
2042        assert_eq!(refcount(b), None);
2043        assert_eq!(refcount(shared), Some(1));
2044        assert_eq!(refcount(c), Some(2));
2045        assert_eq!(refcount(d), Some(1));
2046        release(c);
2047        assert!(collect_cycles() >= 2);
2048        assert_eq!(refcount(c), None);
2049        assert_eq!(refcount(d), None);
2050        release(shared);
2051        assert_eq!(refcount(shared), None);
2052    }
2053
2054    /// C5: an object owns the `randc` cycle state of its own fields, keyed by
2055    /// word offset; a copy starts a fresh cycle, and a dead handle has none.
2056    #[test]
2057    fn randc_state_is_per_object_per_offset_and_a_copy_starts_fresh() {
2058        let t = ClassTemplateId(1013);
2059        let a = alloc(t, 2);
2060        let b = alloc(t, 2);
2061        assert_eq!(randc_state(a, 0), Some(RandcState::default()));
2062        let st = RandcState {
2063            live: true,
2064            fingerprint: 7,
2065            remaining: vec![3, 1],
2066        };
2067        assert!(set_randc_states(a, vec![(1, st.clone())]));
2068        assert_eq!(randc_state(a, 1), Some(st));
2069        assert_eq!(randc_state(a, 0), Some(RandcState::default()));
2070        assert_eq!(randc_state(b, 1), Some(RandcState::default()), "per object");
2071        let c = copy(a).unwrap();
2072        assert_eq!(randc_state(c, 1), Some(RandcState::default()), "fresh copy");
2073        release(a);
2074        assert_eq!(randc_state(a, 1), None);
2075        assert!(!set_randc_states(a, vec![(1, RandcState::default())]));
2076        release(b);
2077        release(c);
2078    }
2079}