vj-sim 0.3.0

Netlist, scheduler and process model of the vj SystemVerilog simulator.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
//! Task 2 — SoA arena storage, word-alignment, and the FFI arena view (ledger
//! ST-007, ST-008, ST-009, ST-038, ST-039, ST-145).
//!
//! This is the one design-wide flat Structure-of-Arrays backing store
//! ARCHITECTURE §6.2/state.md's "two tiers, hard 64-bit boundary" describes:
//! signals with `bit_width <= 64` live inline as a single packed `u128` word
//! (val low 64 bits / xzmask high 64 bits, `sv_lang::Bit`/`Bit4` convention —
//! see `state::value`), signals wider than 64 bits (and all memories) live in
//! two parallel `Vec<u64>` planes (`wide_aval`/`wide_bval`), word-aligned, no
//! cross-signal bit-packing (ST-038: even a 1-bit wire gets a full `u128`).
//!
//! No signal is ever represented as a per-signal heap object (ST-007): the
//! only fields on [`Arena`] are the five flat backing `Vec`s themselves — no
//! `HashMap`/`BTreeMap` anywhere in this file.
//!
//! **Review fix (Task 7 r1): interior mutability, not `&mut self`, for every
//! accessor that grows or writes through the backing `Vec`s.** Task 7's
//! `InstanceHandle::arena` mints a `*const Arena` once and reuses it for the
//! life of the simulation run — including across every *later*
//! `StateEngine::allocate_instance` call and (from Task 8 on) `commit_write`.
//! Under Rust's Stacked/Tree Borrows aliasing model, forming a fresh
//! `&mut Arena` *at all* (even one that never writes) invalidates every
//! previously-derived raw pointer's borrow-stack tag on `Arena`'s own field
//! memory — confirmed by `cargo +nightly miri test -p vj-sim instance::`
//! failing on exactly that pattern before this fix. The five backing `Vec`s
//! are therefore each wrapped in [`std::cell::UnsafeCell`] and every method
//! that grows or writes through one takes `&self`: growing/writing methods
//! are `unsafe fn` (the caller must uphold the single-logical-writer
//! discipline [`Arena::raw_inline_ptr`] already documented, and must not
//! hold a live `&[T]`/`&mut [T]` from any other accessor across the call);
//! read-only methods stay safe, since two live `&[T]` reads never alias.
//! `self`'s own type therefore never again needs a `&mut Arena` reborrow
//! after a `Box<Arena>` is constructed, so a `*const Arena` minted once
//! stays valid. (This still assumes no code takes `&mut *engine.arena`
//! directly through `StateEngine`'s `pub arena: Box<Arena>` field, bypassing
//! this module's methods entirely — that remains a documented trust
//! boundary of the `pub` field, not something this module can prevent.)

use std::cell::{Cell, UnsafeCell};
use std::ptr;

/// Whether an [`Arena`] backs a 4-state (`aval`/`bval`, X/Z-capable) or
/// 2-state (`inline_2s`/`wide_2s`, val-only) compiled binary. Feeds Task 10's
/// 2-state build path; this task only allocates/exposes the storage, it does
/// not yet implement 2-state read/write semantics.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum BuildMode {
    FourState,
    TwoState,
}

/// The one design-wide flat SoA backing store (ST-007: grouped by storage
/// tier, not a per-signal heap object model). `inline` holds every
/// `bit_width <= 64` signal as a packed `u128` (val low 64 / xzmask high 64);
/// `wide_aval`/`wide_bval` hold every wider signal and every memory element,
/// word-aligned, at identical relative offsets in both planes (ST-009);
/// `inline_2s`/`wide_2s` are the 2-state-build counterparts (Task 10 exercises
/// them; this task only reserves the fields).
///
/// Each backing `Vec` is wrapped in [`UnsafeCell`] (review fix, Task 7 r1):
/// see the module-level doc comment for why growing/writing must never
/// require a fresh `&mut Arena`.
///
/// **Task 20 (ST-040) — cross-signal bit-packing is out of scope for v1, by
/// deliberate decision, not a silently-dropped optimization.** Every signal
/// gets a full `u128` inline word or full word-aligned wide-plane slot (see
/// the module doc comment's "no cross-signal bit-packing... even a 1-bit
/// wire gets a full `u128`"); nothing in this file ever packs two or more
/// narrow signals into one word's spare bits. This is a **tracked v2 scaling
/// risk** for huge port-heavy designs (a design with millions of 1-bit
/// signals pays a full 16 bytes each rather than the theoretical 1 bit),
/// cross-referenced at `docs/design/state.md` §1/Risks and restated in
/// `docs/CONTRACTS.md`'s "Wave A — wA-state notes" section so it is
/// discoverable without reading this file.
pub struct Arena {
    mode: BuildMode,
    inline: UnsafeCell<Vec<u128>>,
    wide_aval: UnsafeCell<Vec<u64>>,
    wide_bval: UnsafeCell<Vec<u64>>,
    inline_2s: UnsafeCell<Vec<u64>>,
    wide_2s: UnsafeCell<Vec<u64>>,
    /// **Review fix (Task 14 r2, blocking finding 2):** a wide-word plane
    /// pair dedicated exclusively to `driver::force_driver`'s dynamically
    /// -allocated Wide/Memory-class contribution cells (ST-055). Unlike
    /// `wide_aval`/`wide_bval`, nothing in this crate ever hands out a raw
    /// pointer aliasing this Vec's backing storage (`ffi_view`/`sv_get_wide`
    /// never touch it — only [`SlotClass::ForceWide`]-class `SlotLocation`s
    /// address it, and those are only ever minted by `force_driver`, never by
    /// the layout builder or the accessor ABI), so growing/reallocating it
    /// can never dangle an already-issued `WideView`. It is therefore never
    /// gated by `wide_sealed` — `force`/`release` (ST-055) must stay real,
    /// always-available runtime operations even after `sv_get_wide` has
    /// sealed the *ordinary* wide plane (see `alloc_force_wide_words`'s doc
    /// comment for the full argument this fixes).
    force_wide_aval: UnsafeCell<Vec<u64>>,
    force_wide_bval: UnsafeCell<Vec<u64>>,
    /// **Review fix (Task 9 r3): closes a wide-plane use-after-free.**
    /// `accessor_impl.rs::sv_get_wide` returns a `WideView` whose `aval`/
    /// `bval` raw pointers deliberately alias `wide_aval`/`wide_bval`'s own
    /// live storage and are retained by the caller past the call (unlike
    /// every other accessor, which never retains a pointer). Unlike
    /// `inline`, the wide planes are one flat, growable `Vec<u64>` each (not
    /// individually boxed per allocation — the FFI arena view, ST-145,
    /// needs one truly contiguous raw-pointer-addressable buffer per plane,
    /// so `TemplateRegistry`'s per-entry-boxing fix doesn't apply here
    /// without breaking that contract): a later `allocate_instance` call
    /// growing them via `alloc_wide_words` could reallocate the backing
    /// buffer and dangle every previously issued `WideView`. `sv_get_wide`
    /// sets this the moment it hands out such a view (`seal_wide_plane`
    /// below); `alloc_wide_words` then refuses (panics on) any further
    /// nonzero-word growth, turning the hazard into an immediate, loud
    /// failure at the first offending call instead of a silent dangling
    /// pointer. A `words == 0` request stays a legal no-op even once
    /// sealed — it never touches either `Vec`'s capacity, so it can't
    /// reallocate anything (see `alloc_inline_words`'s identical
    /// zero-is-a-no-op contract).
    wide_sealed: Cell<bool>,
    /// Huge-signal mirrors (`state::huge_mirror`): the registry itself lives
    /// there, this is only where an arena keeps it. Every plane writer
    /// invalidates the mirrors it overlaps.
    pub(crate) huge: super::huge_mirror::HugeMirrors,
}

// `UnsafeCell` makes `Arena` `!Sync` by default, which is correct: nothing
// in this module claims Arena is safe to share across threads without
// external synchronization. `raw_inline_ptr`'s own SAFETY comment already
// documents the single-logical-writer-per-offset discipline multi-threaded
// callers (e.g. this file's `concurrent_writes_to_disjoint_words_produce_no_torn_reads`
// test) must uphold by hand.
impl std::fmt::Debug for Arena {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        // SAFETY: reads only each Vec's `.len()` through a shared reference
        // for diagnostic purposes; per this Arena's single-logical-writer
        // invariant (see the module doc comment and `raw_inline_ptr`), a
        // length read that doesn't outlive this call and races no other
        // access is sound the same way `inline()`/`wide_aval()`/`wide_bval()`
        // below are.
        let (inline_len, wide_len, inline_2s_len, wide_2s_len, force_wide_len) = unsafe {
            (
                (*self.inline.get()).len(),
                (*self.wide_aval.get()).len(),
                (*self.inline_2s.get()).len(),
                (*self.wide_2s.get()).len(),
                (*self.force_wide_aval.get()).len(),
            )
        };
        f.debug_struct("Arena")
            .field("mode", &self.mode)
            .field("inline_len", &inline_len)
            .field("wide_len", &wide_len)
            .field("inline_2s_len", &inline_2s_len)
            .field("wide_2s_len", &wide_2s_len)
            .field("force_wide_len", &force_wide_len)
            .finish()
    }
}

impl Arena {
    pub fn new(mode: BuildMode) -> Self {
        Arena {
            mode,
            inline: UnsafeCell::new(Vec::new()),
            wide_aval: UnsafeCell::new(Vec::new()),
            wide_bval: UnsafeCell::new(Vec::new()),
            inline_2s: UnsafeCell::new(Vec::new()),
            wide_2s: UnsafeCell::new(Vec::new()),
            force_wide_aval: UnsafeCell::new(Vec::new()),
            force_wide_bval: UnsafeCell::new(Vec::new()),
            wide_sealed: Cell::new(false),
            huge: super::huge_mirror::HugeMirrors::default(),
        }
    }

    /// **Review fix (Task 9 r3):** called by `accessor_impl.rs::sv_get_wide`
    /// the instant it hands out a `WideView` aliasing the wide planes' live
    /// storage — see [`Self::wide_sealed`]'s doc comment. Idempotent and
    /// safe (`Cell::set`, no aliasing concern): any number of `sv_get_wide`
    /// calls, from any number of instances, just keep it set.
    pub(crate) fn seal_wide_plane(&self) {
        self.wide_sealed.set(true);
    }

    /// Whether [`Self::seal_wide_plane`] has fired — exposed so tests can
    /// assert the seal state directly rather than only through
    /// `alloc_wide_words`'s panic message.
    #[cfg(test)]
    pub(crate) fn is_wide_plane_sealed(&self) -> bool {
        self.wide_sealed.get()
    }

    /// Appends `n` new inline `u128` words in one call, returning the shared
    /// start offset (Task 7, ST-067/ST-136's bulk variant of
    /// [`Self::alloc_inline_word`] — reserves a whole template instance's
    /// inline words in one bump, the same way [`Self::alloc_wide_words`]
    /// already bulk-allocates the wide planes). `n == 0` is a legal no-op
    /// that still returns the current length (a template with zero inline
    /// words gets a well-defined, if unused, base).
    ///
    /// # Safety
    /// The caller must ensure no other live `&`/`&mut` borrow of the inline
    /// plane (from [`Self::inline`], [`Self::inline_mut`], or
    /// [`Self::raw_inline_ptr`]) is held across this call, and that at most
    /// one logical writer grows this Arena's planes at a time (the same
    /// single-logical-writer discipline [`Self::raw_inline_ptr`] documents).
    /// This method takes `&self`, not `&mut self`, specifically so it can be
    /// called through a `*const Arena` minted once (e.g.
    /// `InstanceHandle::arena`) without ever forming a fresh `&mut Arena` —
    /// see the module-level doc comment.
    pub unsafe fn alloc_inline_words(&self, n: u32) -> u32 {
        // SAFETY: forwarded to this function's own `# Safety` section above.
        unsafe {
            let inline = &mut *self.inline.get();
            let inline_2s = &mut *self.inline_2s.get();
            let off = inline.len() as u32;
            for _ in 0..n {
                inline.push(0);
                inline_2s.push(0);
            }
            off
        }
    }

    /// Appends one new inline `u128` word, returning its `word_offset`
    /// (always `len`-before-push — dense, order-preserving, never a reused or
    /// coalesced gap: ST-038's "even a single-bit wire gets a full word").
    /// Task 7: a thin wrapper over [`Self::alloc_inline_words`]`(1)`.
    ///
    /// # Safety
    /// Same obligations as [`Self::alloc_inline_words`].
    pub unsafe fn alloc_inline_word(&self) -> u32 {
        // SAFETY: forwarded to `alloc_inline_words`'s `# Safety` section.
        unsafe { self.alloc_inline_words(1) }
    }

    /// Appends `words` new `u64` slots to both wide planes in lockstep,
    /// returning the shared start offset (ST-009: the same relative
    /// addressing scheme in both planes).
    ///
    /// # Panics
    /// **Review fix (Task 9 r3):** if `words > 0` and [`Self::seal_wide_plane`]
    /// has already been called on this `Arena` (i.e. `sv_get_wide` has
    /// already handed some caller a `WideView` aliasing these planes' live
    /// storage) — growing either `Vec` now could reallocate its backing
    /// buffer and dangle that already-issued `WideView`, so this refuses
    /// rather than silently doing it. See [`Self::wide_sealed`]'s doc
    /// comment for the full hazard. `words == 0` is exempt (it never
    /// touches either `Vec`'s capacity, so it can never reallocate
    /// anything) and stays the legal, always-safe no-op it already was.
    ///
    /// # Safety
    /// The caller must ensure no other live `&`/`&mut` borrow of either wide
    /// plane (from [`Self::wide_aval`]/[`Self::wide_bval`]/
    /// [`Self::wide_aval_mut`]/[`Self::wide_bval_mut`]) is held across this
    /// call, and that at most one logical writer grows this Arena's planes
    /// at a time. `&self`, not `&mut self`, for the same reason as
    /// [`Self::alloc_inline_words`].
    pub unsafe fn alloc_wide_words(&self, words: u32) -> u32 {
        assert!(
            words == 0 || !self.wide_sealed.get(),
            "Arena::alloc_wide_words: refusing to grow the wide plane -- \
             sv_get_wide has already handed out a WideView aliasing this \
             Arena's wide_aval/wide_bval live storage, and growing them now \
             would reallocate that storage and dangle it (a use-after-free); \
             no further wide-word allocation (e.g. via allocate_instance) is \
             legal against this Arena once any sv_get_wide call has been made"
        );
        // SAFETY: forwarded to this function's own `# Safety` section above.
        unsafe {
            let wide_aval = &mut *self.wide_aval.get();
            let wide_bval = &mut *self.wide_bval.get();
            let wide_2s = &mut *self.wide_2s.get();
            let off = wide_aval.len() as u32;
            for _ in 0..words {
                wide_aval.push(0);
                wide_bval.push(0);
                wide_2s.push(0);
            }
            off
        }
    }

    /// **Review fix (Task 14 r2, blocking finding 2):** the `force_wide_aval`/
    /// `force_wide_bval` counterpart of [`Self::alloc_wide_words`] —
    /// `driver::force_driver`'s Wide/Memory-class contribution-cell
    /// allocator. Deliberately **never** consults [`Self::wide_sealed`]: that
    /// flag exists only to protect `WideView`s `sv_get_wide` has handed out
    /// aliasing `wide_aval`/`wide_bval`'s storage, and this method never
    /// grows those planes at all — it grows a wholly separate pair that no
    /// accessor-ABI function or `ffi_view` call ever exposes a pointer into
    /// (see this field pair's own doc comment on [`Arena`]). Growing/
    /// reallocating it is therefore unconditionally safe, `wide_sealed` or
    /// not, which is exactly what makes `force`/`release` real, always
    /// -available runtime operations (ST-055) rather than ones that panic the
    /// instant any wide signal has been read through `sv_get_wide` first —
    /// the bug this fixes (Task 14 review finding: `force_driver` on a net
    /// wider than 64 bits panicked via `alloc_wide_words` once the ordinary
    /// wide plane had been sealed by an unrelated prior `sv_get_wide` call).
    ///
    /// # Safety
    /// Same obligations as [`Self::alloc_wide_words`], applied to this
    /// separate plane pair.
    pub unsafe fn alloc_force_wide_words(&self, words: u32) -> u32 {
        // SAFETY: forwarded to this function's own `# Safety` section above.
        unsafe {
            let force_wide_aval = &mut *self.force_wide_aval.get();
            let force_wide_bval = &mut *self.force_wide_bval.get();
            let off = force_wide_aval.len() as u32;
            for _ in 0..words {
                force_wide_aval.push(0);
                force_wide_bval.push(0);
            }
            off
        }
    }

    /// Safe: see [`Self::inline`]'s doc comment — the same reasoning applies
    /// to the force-only wide `aval` plane ([`Self::alloc_force_wide_words`]'s
    /// doc comment).
    pub fn force_wide_aval(&self) -> &[u64] {
        // SAFETY: see `inline()`'s SAFETY comment; identical reasoning.
        unsafe { &*self.force_wide_aval.get() }
    }

    /// Safe: see [`Self::inline`]'s doc comment — the same reasoning applies
    /// to the force-only wide `bval` plane ([`Self::alloc_force_wide_words`]'s
    /// doc comment).
    pub fn force_wide_bval(&self) -> &[u64] {
        // SAFETY: see `inline()`'s SAFETY comment; identical reasoning.
        unsafe { &*self.force_wide_bval.get() }
    }

    /// # Safety
    /// Same obligations as [`Self::inline_mut`], applied to the force-only
    /// wide `aval` plane.
    #[allow(clippy::mut_from_ref)]
    pub unsafe fn force_wide_aval_mut(&self) -> &mut [u64] {
        // SAFETY: forwarded to this function's own `# Safety` section above.
        unsafe { &mut *self.force_wide_aval.get() }
    }

    /// # Safety
    /// Same obligations as [`Self::inline_mut`], applied to the force-only
    /// wide `bval` plane.
    #[allow(clippy::mut_from_ref)]
    pub unsafe fn force_wide_bval_mut(&self) -> &mut [u64] {
        // SAFETY: forwarded to this function's own `# Safety` section above.
        unsafe { &mut *self.force_wide_bval.get() }
    }

    /// Task 13 (ST-100, ST-139): the 2-state-family name for the same bulk
    /// growth [`Self::alloc_inline_words`] already performs on the
    /// `inline_2s` plane. **Deviation from the plan's literal prose** ("this
    /// task adds their allocators", implying a growth operation independent
    /// of the 4-state family's own bulk allocators): `alloc_inline_words`
    /// already grows `inline`/`inline_2s` in lockstep (see its own body
    /// above), which is load-bearing, not incidental -- a `SlotOffset`'s
    /// `word_offset` is computed once by the mode-agnostic Task 4/6 layout
    /// builder and must address the *same* position regardless of which
    /// build mode the compiled binary ends up using, so the two planes'
    /// offset spaces cannot be allowed to diverge. A genuinely independent
    /// `inline_2s`-only counter would break that invariant the first time a
    /// caller allocated through one family without the other. This method is
    /// therefore a thin, same-offset forwarding wrapper -- a 2-state-family
    /// name for API discoverability/completeness, not a second allocator.
    ///
    /// # Safety
    /// Same obligations as [`Self::alloc_inline_words`].
    pub unsafe fn alloc_inline_2s_words(&self, n: u32) -> u32 {
        // SAFETY: forwarded to `alloc_inline_words`'s `# Safety` section.
        unsafe { self.alloc_inline_words(n) }
    }

    /// Task 13 (ST-100, ST-139): the wide-plane counterpart of
    /// [`Self::alloc_inline_2s_words`] -- see that method's doc comment for
    /// why this forwards to [`Self::alloc_wide_words`] rather than growing
    /// `wide_2s` independently.
    ///
    /// # Safety
    /// Same obligations as [`Self::alloc_wide_words`].
    pub unsafe fn alloc_wide_2s_words(&self, n: u32) -> u32 {
        // SAFETY: forwarded to `alloc_wide_words`'s `# Safety` section.
        unsafe { self.alloc_wide_words(n) }
    }

    /// Read-only access to the inline plane (test/read helper — the hot path
    /// is the FFI raw-pointer view below). Safe: any number of live `&[T]`
    /// reads coexist without aliasing; the aliasing obligation is on the
    /// `unsafe` mutating accessors instead (see their doc comments).
    pub fn inline(&self) -> &[u128] {
        // SAFETY: produces a shared slice from the `UnsafeCell`. Multiple
        // concurrent `&[u128]` reads never conflict with each other; the
        // caller of any mutating accessor (`alloc_inline_words`,
        // `inline_mut`, `raw_inline_ptr`) is the one obligated not to hold a
        // read like this one live across their call (documented on each).
        unsafe { &*self.inline.get() }
    }

    /// Mutable access to the inline plane.
    ///
    /// # Safety
    /// The caller must ensure no other live `&`/`&mut` borrow of the inline
    /// plane exists for as long as the returned slice is used, and that no
    /// other writer touches it concurrently (single-logical-writer, same as
    /// [`Self::raw_inline_ptr`]). `&self`, not `&mut self`, for the same
    /// reason as [`Self::alloc_inline_words`] — see the module-level doc
    /// comment.
    // `clippy::mut_from_ref` flags `fn(&self) -> &mut _` on sight as a
    // near-universal UB source; here it's the deliberate `UnsafeCell`
    // interior-mutability pattern this whole module documents (the
    // aliasing obligation is pushed onto the caller via `# Safety` above,
    // not silently ignored), so it's a known false positive for this shape.
    #[allow(clippy::mut_from_ref)]
    pub unsafe fn inline_mut(&self) -> &mut [u128] {
        // SAFETY: forwarded to this function's own `# Safety` section above.
        unsafe { &mut *self.inline.get() }
    }

    /// Safe: see [`Self::inline`]'s doc comment — the same reasoning applies
    /// to the wide `aval` plane.
    pub fn wide_aval(&self) -> &[u64] {
        // SAFETY: see `inline()`'s SAFETY comment; identical reasoning.
        unsafe { &*self.wide_aval.get() }
    }

    /// Safe: see [`Self::inline`]'s doc comment — the same reasoning applies
    /// to the wide `bval` plane.
    pub fn wide_bval(&self) -> &[u64] {
        // SAFETY: see `inline()`'s SAFETY comment; identical reasoning.
        unsafe { &*self.wide_bval.get() }
    }

    /// # Safety
    /// Same obligations as [`Self::inline_mut`], applied to the wide `aval`
    /// plane.
    // See `inline_mut`'s `clippy::mut_from_ref` comment — same reasoning.
    #[allow(clippy::mut_from_ref)]
    pub unsafe fn wide_aval_mut(&self) -> &mut [u64] {
        // SAFETY: forwarded to this function's own `# Safety` section above.
        unsafe { &mut *self.wide_aval.get() }
    }

    /// # Safety
    /// Same obligations as [`Self::inline_mut`], applied to the wide `bval`
    /// plane.
    // See `inline_mut`'s `clippy::mut_from_ref` comment — same reasoning.
    #[allow(clippy::mut_from_ref)]
    pub unsafe fn wide_bval_mut(&self) -> &mut [u64] {
        // SAFETY: forwarded to this function's own `# Safety` section above.
        unsafe { &mut *self.wide_bval.get() }
    }

    /// **Review fix (Task 9 r4, blocking finding): the Stacked-Borrows-safe
    /// way to obtain a pointer into the wide `aval` plane that the caller
    /// retains and dereferences *after* this call returns** (unlike every
    /// other accessor on this type, which is used and discarded within a
    /// single call). [`Self::wide_aval_mut`] is unsound for this purpose:
    /// under Rust's Stacked Borrows aliasing model, `&mut *self.wide_aval
    /// .get()` (plus the `Vec<u64> -> &mut [u64]` deref coercion
    /// `wide_aval_mut`'s return type triggers) retags the plane's backing
    /// buffer with a *fresh* `Unique` tag on **every call** — a raw pointer
    /// formed from *that* reborrow (e.g. via `wide_aval_mut()[start..]
    /// .as_mut_ptr()`, the pattern `sv_get_wide` used before this fix) is
    /// invalidated the very next time *any* code calls `wide_aval_mut()`
    /// again for this `Arena` (a later `sv_get_wide` for a different slot,
    /// or a wide-class `commit_write_at` write via [`Self::wide_aval_mut`]
    /// in `write::write_slot`) — confirmed via an isolated Miri repro of
    /// exactly this pattern (see this crate's Task 9 r4 fix commit).
    ///
    /// This method instead reads `Vec::as_mut_ptr()` directly off the
    /// `UnsafeCell`-derived place (`(*self.wide_aval.get()).as_mut_ptr()`)
    /// **without ever forming a `&mut [u64]`/`&[u64]` reference over the
    /// buffer** — `Vec::as_mut_ptr()`'s only effect is reading the `Vec`'s
    /// own `ptr` field (a plain pointer *value* copy, not a reborrow of the
    /// heap buffer it points to), so the returned pointer keeps the
    /// buffer's original allocation tag every time this is called, for the
    /// life of the `Arena` (as long as [`Self::seal_wide_plane`] has fixed
    /// the buffer's address — see [`Self::alloc_wide_words`]'s panic
    /// guard). Because that tag sits at the *bottom* of the buffer's borrow
    /// stack, it survives every later child reborrow pushed on top of it by
    /// `wide_aval_mut`/`wide_aval`/`wide_aval_mut()`-based writes and
    /// reads elsewhere (verified: Miri accepts a pointer obtained this way
    /// being read/written both before *and* after such an intervening
    /// slice-based access to the same plane, disjoint range or not).
    /// Pointer arithmetic (`.add(offset)`) against the returned base
    /// pointer, never slicing, is the caller's obligation for the same
    /// reason.
    ///
    /// # Safety
    /// The Arena's wide planes must already be sealed ([`Self::seal_wide_plane`])
    /// or the caller must otherwise guarantee no later [`Self::alloc_wide_words`]
    /// call reallocates the buffer while this pointer is retained (same
    /// reallocation hazard [`Self::wide_sealed`]'s doc comment describes);
    /// `offset` plus however many words the caller reads/writes through the
    /// result must stay within the plane's allocated length.
    pub(crate) unsafe fn wide_aval_ptr(&self) -> *mut u64 {
        // SAFETY: forwarded to this function's own `# Safety` section above;
        // reads only the `Vec`'s own `ptr` field (via the auto-ref
        // `as_mut_ptr()`'s `&mut self` receiver requires, scoped to this
        // call and touching only the `Vec` struct's own memory, never the
        // heap buffer) and returns it by value — no `&mut [u64]`/`&[u64]`
        // over the buffer is ever formed here.
        unsafe { (*self.wide_aval.get()).as_mut_ptr() }
    }

    /// The `wide_bval` twin of [`Self::wide_aval_ptr`] — see its doc comment
    /// for the full Stacked-Borrows argument, identical here.
    ///
    /// # Safety
    /// Same obligations as [`Self::wide_aval_ptr`], applied to the `wide_bval`
    /// plane.
    pub(crate) unsafe fn wide_bval_ptr(&self) -> *mut u64 {
        // SAFETY: see `wide_aval_ptr`'s SAFETY comment; identical reasoning.
        unsafe { (*self.wide_bval.get()).as_mut_ptr() }
    }

    /// Task 13 (ST-100, ST-102, ST-139): read-only access to the 2-state
    /// inline plane -- see [`Self::inline`]'s doc comment, identical
    /// reasoning.
    pub fn inline_2s(&self) -> &[u64] {
        // SAFETY: see `inline()`'s SAFETY comment; identical reasoning.
        unsafe { &*self.inline_2s.get() }
    }

    /// # Safety
    /// Same obligations as [`Self::inline_mut`], applied to the `inline_2s`
    /// plane.
    // See `inline_mut`'s `clippy::mut_from_ref` comment — same reasoning.
    #[allow(clippy::mut_from_ref)]
    pub unsafe fn inline_2s_mut(&self) -> &mut [u64] {
        // SAFETY: forwarded to this function's own `# Safety` section above.
        unsafe { &mut *self.inline_2s.get() }
    }

    /// Task 13 (ST-100, ST-102, ST-139): read-only access to the 2-state
    /// wide plane -- see [`Self::inline`]'s doc comment, identical
    /// reasoning.
    pub fn wide_2s(&self) -> &[u64] {
        // SAFETY: see `inline()`'s SAFETY comment; identical reasoning.
        unsafe { &*self.wide_2s.get() }
    }

    /// # Safety
    /// Same obligations as [`Self::inline_mut`], applied to the `wide_2s`
    /// plane.
    // See `inline_mut`'s `clippy::mut_from_ref` comment — same reasoning.
    #[allow(clippy::mut_from_ref)]
    pub unsafe fn wide_2s_mut(&self) -> &mut [u64] {
        // SAFETY: forwarded to this function's own `# Safety` section above.
        unsafe { &mut *self.wide_2s.get() }
    }

    pub fn mode(&self) -> BuildMode {
        self.mode
    }

    /// The FFI arena view (ST-145): raw pointers into the three planes a
    /// compiled kernel indexes through the accessor ABI. `wide_aval`/
    /// `wide_bval` are null when `mode == TwoState` — the 4-state planes
    /// simply aren't touched in a 2-state build. Takes `&mut self`: this is
    /// Task 2's original, not-yet-consumed-anywhere entry point (no Task 7
    /// code path calls it against an `Arena` that already has live
    /// `InstanceHandle`s pointing at it), so it keeps requiring genuine
    /// exclusive access rather than adopting Task 7's `&self`/`UnsafeCell`
    /// pattern; `UnsafeCell::get_mut()` is a safe way to reach the backing
    /// `Vec`s given that exclusive access.
    pub fn ffi_view(&mut self) -> StateArena {
        StateArena {
            inline: self.inline.get_mut().as_mut_ptr(),
            wide_aval: if self.mode == BuildMode::TwoState {
                ptr::null_mut()
            } else {
                self.wide_aval.get_mut().as_mut_ptr()
            },
            wide_bval: if self.mode == BuildMode::TwoState {
                ptr::null_mut()
            } else {
                self.wide_bval.get_mut().as_mut_ptr()
            },
        }
    }

    /// Raw pointer to the start of the inline plane, for callers that need to
    /// hand out disjoint per-word pointers to multiple worker threads
    /// (ST-039: word-aligned, non-packed slots enable lock-free writes across
    /// worker threads with no per-write locking). Takes `&mut self` for the
    /// same reason [`Self::ffi_view`] does — see its doc comment.
    ///
    /// # Safety
    /// The caller writes only through offsets it obtained from this Arena's
    /// own `alloc_*` calls, each offset written by at most one thread for the
    /// duration of the unsafe scope, and must not alias with any `&Arena`/
    /// `&mut Arena` use until the writer threads have joined.
    pub unsafe fn raw_inline_ptr(&mut self) -> *mut u128 {
        self.inline.get_mut().as_mut_ptr()
    }
}

/// `#[repr(C)]` FFI view of the arena's storage planes (ST-145) — the shape a
/// compiled kernel's accessor-ABI functions index through. `wide_aval`/
/// `wide_bval` are `null` in a 2-state-compiled binary.
#[repr(C)]
pub struct StateArena {
    pub inline: *mut u128,
    pub wide_aval: *mut u64,
    pub wide_bval: *mut u64,
}

#[cfg(test)]
mod tests {
    use super::*;

    /// ST-007: a `HashMap<SignalId, Box<Signal>>`-shaped implementation has
    /// no way to produce this dense, order-preserving sequence from a bare
    /// counter — this is the structural proof of "SoA arena, not a
    /// per-signal heap object model."
    #[test]
    fn alloc_inline_word_is_contiguous_no_per_signal_heap_object() {
        let arena = Arena::new(BuildMode::FourState);
        // SAFETY: `arena` is a local, single-owned value with no other live
        // borrow and no concurrent caller; each call is sequential.
        let offsets: Vec<u32> = (0..5)
            .map(|_| unsafe { arena.alloc_inline_word() })
            .collect();
        assert_eq!(offsets, vec![0, 1, 2, 3, 4]);
    }

    /// ST-008: writing a `bits_to_four_state`-derived word to the inline
    /// plane and reading it back through the fixed low64/high64 split must
    /// match the same val/unk pair the value-layer test computed. Fails if
    /// `alloc_inline_word`'s slot were narrower than 128 bits or misaligned.
    #[cfg(feature = "frontend")] // uses the sv-lang `bits_to_four_state` converter
    #[test]
    fn inline_word_low64_high64_matches_bits_to_four_state() {
        use super::super::{FourStateValue, bits_to_four_state};
        use sv_lang::Bit;

        let fv = bits_to_four_state(&[Bit::One, Bit::X, Bit::Zero, Bit::Z]);
        let (expected_val, expected_unk) = match fv {
            FourStateValue::Narrow(w) => (w as u64, (w >> 64) as u64),
            FourStateValue::Wide { .. } => panic!("expected Narrow for 4 bits"),
        };

        let arena = Arena::new(BuildMode::FourState);
        // SAFETY: `arena` is a local, single-owned value; no other borrow of
        // the inline plane is live across these sequential calls.
        let off = unsafe { arena.alloc_inline_word() };
        unsafe {
            arena.inline_mut()[off as usize] =
                (expected_val as u128) | ((expected_unk as u128) << 64);
        }

        let word = arena.inline()[off as usize];
        let (val, unk) = (word as u64, (word >> 64) as u64);
        assert_eq!(val, expected_val);
        assert_eq!(unk, expected_unk);
    }

    /// ST-009: both wide planes must use the exact same relative addressing
    /// scheme — allocating for a second signal never overlaps the first
    /// signal's region in either plane.
    #[test]
    fn wide_planes_share_identical_relative_offsets() {
        let arena = Arena::new(BuildMode::FourState);
        // SAFETY: `arena` is a local, single-owned value; no other borrow of
        // either wide plane is live across these sequential calls.
        let o1 = unsafe { arena.alloc_wide_words(2) };
        unsafe {
            arena.wide_aval_mut()[o1 as usize] = 0xAAAA_AAAA_AAAA_AAAA;
            arena.wide_bval_mut()[o1 as usize] = 0xBBBB_BBBB_BBBB_BBBB;
            arena.wide_aval_mut()[(o1 + 1) as usize] = 0xCCCC_CCCC_CCCC_CCCC;
            arena.wide_bval_mut()[(o1 + 1) as usize] = 0xDDDD_DDDD_DDDD_DDDD;
        }

        let o2 = unsafe { arena.alloc_wide_words(2) };
        assert_eq!(o2, o1 + 2);

        // The second signal's allocation must not have touched the first
        // signal's already-written region in either plane.
        assert_eq!(arena.wide_aval()[o1 as usize], 0xAAAA_AAAA_AAAA_AAAA);
        assert_eq!(arena.wide_bval()[o1 as usize], 0xBBBB_BBBB_BBBB_BBBB);
        assert_eq!(arena.wide_aval()[(o1 + 1) as usize], 0xCCCC_CCCC_CCCC_CCCC);
        assert_eq!(arena.wide_bval()[(o1 + 1) as usize], 0xDDDD_DDDD_DDDD_DDDD);

        // The second signal's own region starts fresh (zeroed), proving no
        // overlap the other direction either.
        assert_eq!(arena.wide_aval()[o2 as usize], 0);
        assert_eq!(arena.wide_bval()[o2 as usize], 0);
    }

    /// ST-038: two consecutive 1-bit-wire allocations must land in two
    /// distinct words — a bit-packing implementation trying to co-locate two
    /// 1-bit wires in one word would return the same offset for both,
    /// failing this.
    #[test]
    fn no_two_signals_ever_share_a_64bit_word_even_two_1bit_wires() {
        let arena = Arena::new(BuildMode::FourState);
        // SAFETY: `arena` is a local, single-owned value; no other borrow is
        // live across these sequential calls.
        let a = unsafe { arena.alloc_inline_word() };
        let b = unsafe { arena.alloc_inline_word() };
        assert_eq!(b - a, 1);
        assert_ne!(a, b);
    }

    /// ST-039: word-aligned, non-packed slots must allow multiple threads to
    /// write disjoint words with no locking and no torn reads. Each thread
    /// derives its own raw pointer from one `raw_inline_ptr()` call before
    /// spawning (so the unsafe scope's precondition — one writer per offset,
    /// no aliasing `&Arena`/`&mut Arena` use until join — holds), writes a
    /// distinct pattern, and after `join` every word is read back through the
    /// safe `Vec` API and must match exactly.
    #[test]
    fn concurrent_writes_to_disjoint_words_produce_no_torn_reads() {
        const N: usize = 8;
        let mut arena = Arena::new(BuildMode::FourState);
        for _ in 0..N {
            // SAFETY: sequential, single-owned, no other live borrow.
            unsafe {
                arena.alloc_inline_word();
            }
        }

        let patterns: [u128; N] = [
            0x1111_1111_1111_1111_1111_1111_1111_1111,
            0x2222_2222_2222_2222_2222_2222_2222_2222,
            0x3333_3333_3333_3333_3333_3333_3333_3333,
            0x4444_4444_4444_4444_4444_4444_4444_4444,
            0x5555_5555_5555_5555_5555_5555_5555_5555,
            0x6666_6666_6666_6666_6666_6666_6666_6666,
            0x7777_7777_7777_7777_7777_7777_7777_7777,
            0x8888_8888_8888_8888_8888_8888_8888_8888,
        ];

        // SAFETY: 8 words were pre-allocated above via alloc_inline_word, so
        // offsets 0..8 are all valid. Each spawned thread below receives
        // exactly one derived pointer (ptr.add(i)) and writes only through
        // it; std::thread::scope guarantees the pointers cannot outlive
        // `arena`, and no other &Arena/&mut Arena use happens until after
        // join, so the disjoint-offset, single-writer, non-aliasing
        // precondition documented on raw_inline_ptr holds.
        // Raw pointers aren't `Send` by default; each thread below owns
        // exactly one disjoint derived pointer for the duration of the
        // scope, matching `raw_inline_ptr`'s documented precondition, so
        // wrapping it to cross the thread boundary is sound.
        struct SendPtr(*mut u128);
        // SAFETY: each `SendPtr` is used by exactly one thread to write
        // through a word offset no other thread touches, for the scope's
        // duration only (see the surrounding SAFETY comment).
        unsafe impl Send for SendPtr {}

        let base = unsafe { arena.raw_inline_ptr() };
        std::thread::scope(|s| {
            for (i, &pattern) in patterns.iter().enumerate() {
                let p = SendPtr(unsafe { base.add(i) });
                s.spawn(move || {
                    // Force capture of the whole `SendPtr` (not just its
                    // `.0` field via Rust 2021 disjoint-field capture),
                    // which is what actually makes the closure `Send`.
                    let p = p;
                    // SAFETY: `p` is this thread's own disjoint word offset,
                    // derived from `base` before any thread spawned; see the
                    // scope-level SAFETY comment above.
                    unsafe { ptr::write(p.0, pattern) };
                });
            }
        });

        for (i, &pattern) in patterns.iter().enumerate() {
            assert_eq!(arena.inline()[i], pattern, "torn/wrong write at word {i}");
        }
    }

    /// Task 7 (ST-067/ST-136): `alloc_inline_words(n)` must reserve `n`
    /// contiguous words in one call and return the shared start offset —
    /// the bulk primitive `StateEngine::allocate_instance` needs to give an
    /// 8-instance generate-for loop 8 distinct, non-overlapping bases without
    /// looping `alloc_inline_word()` itself. Also proves the thin-wrapper
    /// relationship: `alloc_inline_word()` followed by `alloc_inline_words(3)`
    /// must land immediately after the single word, with no gap.
    #[test]
    fn alloc_inline_words_reserves_n_contiguous_words_in_one_call() {
        let arena = Arena::new(BuildMode::FourState);
        // SAFETY: `arena` is a local, single-owned value; no other borrow is
        // live across these sequential calls.
        let single = unsafe { arena.alloc_inline_word() };
        assert_eq!(single, 0);

        let bulk_start = unsafe { arena.alloc_inline_words(3) };
        assert_eq!(
            bulk_start, 1,
            "must start immediately after the single word, no gap"
        );
        assert_eq!(arena.inline().len(), 4, "1 single word + 3 bulk words");

        let next = unsafe { arena.alloc_inline_word() };
        assert_eq!(
            next, 4,
            "a later single-word alloc must not overlap the bulk region"
        );
    }

    /// Task 7: a zero-word bulk request is a legal no-op that still returns a
    /// well-defined offset (the current length), never panics and never
    /// perturbs a later allocation's offset.
    #[test]
    fn alloc_inline_words_of_zero_is_a_no_op_returning_current_length() {
        let arena = Arena::new(BuildMode::FourState);
        // SAFETY: `arena` is a local, single-owned value; no other borrow is
        // live across these sequential calls.
        unsafe {
            arena.alloc_inline_word();
        }
        let off = unsafe { arena.alloc_inline_words(0) };
        assert_eq!(off, 1);
        assert_eq!(
            arena.inline().len(),
            1,
            "zero-word request must not grow the arena"
        );

        let next = unsafe { arena.alloc_inline_word() };
        assert_eq!(
            next, 1,
            "the zero-word request must not have consumed an offset"
        );
    }

    /// ST-145: the FFI view's wide planes must be null exactly in 2-state
    /// mode, non-null in 4-state mode (when at least one wide word exists) —
    /// fails if the FFI view always returns the (possibly-empty-but-non-null)
    /// `Vec`'s pointer regardless of mode.
    #[test]
    fn ffi_view_wide_planes_null_only_in_two_state_mode() {
        let mut two_state = Arena::new(BuildMode::TwoState);
        // SAFETY: `two_state` is a local, single-owned value; no other
        // borrow is live across this call.
        unsafe {
            two_state.alloc_wide_words(1);
        }
        let view = two_state.ffi_view();
        assert!(view.wide_aval.is_null());
        assert!(view.wide_bval.is_null());

        let mut four_state = Arena::new(BuildMode::FourState);
        // SAFETY: same as above.
        unsafe {
            four_state.alloc_wide_words(1);
        }
        let view = four_state.ffi_view();
        assert!(!view.wide_aval.is_null());
        assert!(!view.wide_bval.is_null());
    }

    /// Review fix (Task 9 r3, blocking finding 1): before `seal_wide_plane`
    /// is called, `alloc_wide_words` must keep growing normally (the seal
    /// defaults to unset) -- proves the fix doesn't regress the ordinary,
    /// unsealed case any of this module's other tests already rely on.
    #[test]
    fn alloc_wide_words_is_unaffected_before_the_plane_is_sealed() {
        let arena = Arena::new(BuildMode::FourState);
        assert!(!arena.is_wide_plane_sealed());
        // SAFETY: `arena` is a local, single-owned value; no other live
        // borrow of either wide plane across these sequential calls.
        let a = unsafe { arena.alloc_wide_words(3) };
        let b = unsafe { arena.alloc_wide_words(5) };
        assert_eq!(a, 0);
        assert_eq!(b, 3);
    }

    /// Review fix (Task 9 r3, blocking finding 1): once `seal_wide_plane` has
    /// fired, a nonzero-word `alloc_wide_words` call must panic rather than
    /// silently reallocating the backing `Vec<u64>`s -- this is the Arena
    /// -level unit proof of the enforcement `sv_get_wide` now relies on
    /// (`accessor_impl.rs`'s own regression test proves the same thing at the
    /// accessor-ABI level, through a real `WideView`).
    #[test]
    #[should_panic(expected = "refusing to grow the wide plane")]
    fn alloc_wide_words_panics_on_nonzero_growth_once_sealed() {
        let arena = Arena::new(BuildMode::FourState);
        // SAFETY: `arena` is a local, single-owned value; no other live
        // borrow of either wide plane across this call.
        unsafe {
            arena.alloc_wide_words(2);
        }
        arena.seal_wide_plane();
        assert!(arena.is_wide_plane_sealed());
        // SAFETY: this call is expected to panic before touching either
        // `Vec`; no live borrow of either wide plane is held here regardless.
        unsafe {
            arena.alloc_wide_words(1);
        }
    }

    /// Review fix (Task 9 r3): a `words == 0` request must stay a legal
    /// no-op even once sealed -- it never touches either `Vec`'s capacity
    /// (mirrors `alloc_inline_words_of_zero_is_a_no_op_returning_current_length`),
    /// so it can never be the source of a reallocation and must not be
    /// refused just because some `WideView` is live elsewhere.
    #[test]
    fn alloc_wide_words_of_zero_is_still_a_no_op_once_sealed() {
        let arena = Arena::new(BuildMode::FourState);
        // SAFETY: `arena` is a local, single-owned value; no other live
        // borrow of either wide plane across these sequential calls.
        let off = unsafe { arena.alloc_wide_words(4) };
        arena.seal_wide_plane();
        let after_seal = unsafe { arena.alloc_wide_words(0) };
        assert_eq!(
            after_seal,
            off + 4,
            "zero-word request must still return the current length"
        );
        assert_eq!(
            arena.wide_aval().len(),
            4,
            "zero-word request must not grow the arena even when sealed"
        );
    }
}