qubit-json 0.12.4

Resource-aware infrastructure for lenient and strict JSON processing
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
// =============================================================================
//    Copyright (c) 2026 Haixing Hu.
//
//    SPDX-License-Identifier: Apache-2.0
//
//    Licensed under the Apache License, Version 2.0.
// =============================================================================
//! Accounts for auxiliary capacities held by the JCS renderer.

use std::mem::size_of;

use super::jcs_reserve_error::JcsReserveError;
use crate::encode::JcsWorkingMemoryError;

/// Tracks live stack and object member-table capacities.
pub(super) struct JcsWorkingMemory {
    /// Maximum number of bytes that may be held by tracked capacities.
    limit: usize,
    /// Bytes currently held by the traversal stack.
    stack_bytes: usize,
    /// Bytes currently held by object member tables.
    object_bytes: usize,
    /// Largest stack length requested so far, used as the growth high-water
    /// mark.
    stack_max_required_capacity: usize,
    /// Stack depth through which speculative growth stays disabled after
    /// reclaim.
    stack_growth_exact_through: usize,
    /// Largest combined tracked capacity observed so far.
    peak_bytes: usize,
    /// Successful stack growths observed by the allocation diagnostic.
    #[cfg(feature = "jcs-allocation-diagnostics")]
    stack_growth_count: usize,
    /// Successful exact stack replacements observed by the diagnostic.
    #[cfg(feature = "jcs-allocation-diagnostics")]
    stack_replacement_count: usize,
    /// Sum of requested stack capacities, including successful replacements.
    #[cfg(feature = "jcs-allocation-diagnostics")]
    stack_requested_bytes: usize,
    /// Largest actual stack capacity observed by the diagnostic.
    #[cfg(feature = "jcs-allocation-diagnostics")]
    stack_max_actual_bytes: usize,
    /// Successful object-table growths observed by the allocation diagnostic.
    #[cfg(feature = "jcs-allocation-diagnostics")]
    object_growth_count: usize,
    /// Sum of requested object-table capacities observed by the diagnostic.
    #[cfg(feature = "jcs-allocation-diagnostics")]
    object_requested_bytes: usize,
    /// Largest actual object-table capacity observed by the diagnostic.
    #[cfg(feature = "jcs-allocation-diagnostics")]
    object_max_actual_bytes: usize,
}

impl JcsWorkingMemory {
    /// Creates a tracker with the given capacity limit and no reserved memory.
    ///
    /// # Parameters
    ///
    /// - `limit`: Maximum combined byte capacity allowed for tracked
    ///   collections.
    ///
    /// # Returns
    ///
    /// A tracker initialized with zero live and peak capacity.
    #[inline]
    pub(super) const fn new(limit: usize) -> Self {
        Self {
            limit,
            stack_bytes: 0,
            object_bytes: 0,
            stack_max_required_capacity: 0,
            stack_growth_exact_through: 0,
            peak_bytes: 0,
            #[cfg(feature = "jcs-allocation-diagnostics")]
            stack_growth_count: 0,
            #[cfg(feature = "jcs-allocation-diagnostics")]
            stack_replacement_count: 0,
            #[cfg(feature = "jcs-allocation-diagnostics")]
            stack_requested_bytes: 0,
            #[cfg(feature = "jcs-allocation-diagnostics")]
            stack_max_actual_bytes: 0,
            #[cfg(feature = "jcs-allocation-diagnostics")]
            object_growth_count: 0,
            #[cfg(feature = "jcs-allocation-diagnostics")]
            object_requested_bytes: 0,
            #[cfg(feature = "jcs-allocation-diagnostics")]
            object_max_actual_bytes: 0,
        }
    }

    /// Reserves enough capacity for additional traversal-stack values.
    ///
    /// # Type Parameters
    ///
    /// - `T`: Element type stored by the traversal stack.
    ///
    /// # Parameters
    ///
    /// - `values`: Traversal stack whose capacity is tracked.
    /// - `additional`: Number of elements the stack must hold beyond its
    ///   current length.
    ///
    /// # Returns
    ///
    /// `Ok(())` after the stack has enough capacity and its actual allocation
    /// has been added to the live and peak totals.
    ///
    /// # Errors
    ///
    /// Returns `JcsReserveError::Limit` if the resulting tracked capacity
    /// exceeds the configured limit, or `JcsReserveError::Allocation` if
    /// reservation fails.
    pub(super) fn reserve_stack<T>(&mut self, values: &mut Vec<T>, additional: usize) -> Result<(), JcsReserveError> {
        self.reserve::<T, T>(values, additional, true, None)
    }

    /// Reserves enough capacity for additional entries in an object member
    /// table.
    ///
    /// # Type Parameters
    ///
    /// - `T`: Element type stored by the object member table.
    /// - `S`: Element type stored by the traversal stack.
    ///
    /// # Parameters
    ///
    /// - `values`: Object member table whose capacity is tracked.
    /// - `additional`: Number of entries the table must hold beyond its current
    ///   length.
    /// - `stack`: Traversal stack whose unused capacity may be reclaimed if
    ///   needed to keep an otherwise acceptable member table within budget.
    ///
    /// # Returns
    ///
    /// `Ok(())` after the member table has enough capacity and its actual
    /// allocation has been added to the live and peak totals.
    ///
    /// # Errors
    ///
    /// Returns `JcsReserveError::Limit` if the resulting tracked capacity
    /// exceeds the configured limit, or `JcsReserveError::Allocation` if
    /// reservation fails.
    pub(super) fn reserve_object_entries<T, S>(
        &mut self,
        values: &mut Vec<T>,
        additional: usize,
        stack: &mut Vec<S>,
    ) -> Result<(), JcsReserveError> {
        self.reserve(values, additional, false, Some(stack))
    }

    /// Removes an object's member-table capacity from the tracked live total.
    ///
    /// # Type Parameters
    ///
    /// - `T`: Element type stored by the released object member table.
    ///
    /// # Parameters
    ///
    /// - `values`: Object member table whose capacity is no longer live.
    ///
    /// # Returns
    ///
    /// `Ok(())` after removing the vector's capacity from the live object
    /// total.
    ///
    /// # Errors
    ///
    /// The caller must invoke this before dropping the corresponding vector.
    /// Returns a limit error if its capacity cannot be represented or is
    /// not currently tracked.
    pub(super) fn release_object_entries<T>(&mut self, values: &Vec<T>) -> Result<(), JcsReserveError> {
        let bytes = values
            .capacity()
            .checked_mul(size_of::<T>())
            .ok_or_else(|| self.limit_error(usize::MAX))?;
        self.object_bytes = self
            .object_bytes
            .checked_sub(bytes)
            .ok_or_else(|| self.limit_error(usize::MAX))?;
        Ok(())
    }

    /// Prints one renderer capacity report when the dedicated diagnostic is
    /// enabled.
    ///
    /// # Parameters
    ///
    /// - `success`: Whether rendering completed successfully.
    #[cfg(feature = "jcs-allocation-diagnostics")]
    pub(super) fn report_diagnostics(&self, success: bool) {
        if std::env::var_os("JCS_RENDERER_ALLOCATION_DIAGNOSTICS").is_some() {
            eprintln!(
                "JCS_RENDERER status={} stack_growths={} stack_replacements={} stack_requested_bytes={} stack_max_actual_bytes={} object_growths={} object_requested_bytes={} object_max_actual_bytes={}",
                if success { "success" } else { "failure" },
                self.stack_growth_count,
                self.stack_replacement_count,
                self.stack_requested_bytes,
                self.stack_max_actual_bytes,
                self.object_growth_count,
                self.object_requested_bytes,
                self.object_max_actual_bytes,
            );
        }
    }

    #[cfg(test)]
    fn peak_bytes(&self) -> usize {
        self.peak_bytes
    }

    /// Reserves capacity and updates the tracked bytes for the selected
    /// collection.
    ///
    /// # Type Parameters
    ///
    /// - `T`: Element type stored by the collection being reserved.
    /// - `S`: Element type stored by the optional traversal stack used for
    ///   reclaiming spare capacity.
    ///
    /// # Parameters
    ///
    /// - `values`: Collection to reserve and account for.
    /// - `additional`: Number of elements required beyond the current length.
    /// - `is_stack`: Whether the collection contributes to stack or object
    ///   bytes.
    /// - `stack`: Stack available for reclaiming spare capacity when reserving
    ///   an object table; absent when reserving the stack itself.
    ///
    /// # Returns
    ///
    /// `Ok(())` after capacity is sufficient and the live and peak totals have
    /// been updated.
    ///
    /// # Errors
    ///
    /// The old capacity is excluded while checking the replacement allocation.
    /// Returns a limit error on arithmetic overflow or when the requested or
    /// actual capacity exceeds the configured limit, and an allocation
    /// error if reserving memory fails.
    fn reserve<T, S>(
        &mut self,
        values: &mut Vec<T>,
        additional: usize,
        is_stack: bool,
        mut stack: Option<&mut Vec<S>>,
    ) -> Result<(), JcsReserveError> {
        let required_capacity = values
            .len()
            .checked_add(additional)
            .ok_or_else(|| self.limit_error(usize::MAX))?;
        if is_stack {
            self.stack_max_required_capacity = self.stack_max_required_capacity.max(required_capacity);
        }
        if required_capacity <= values.capacity() {
            return Ok(());
        }

        let old_bytes = values
            .capacity()
            .checked_mul(size_of::<T>())
            .ok_or_else(|| self.limit_error(usize::MAX))?;
        let current = self
            .stack_bytes
            .checked_add(self.object_bytes)
            .ok_or_else(|| self.limit_error(usize::MAX))?;
        let mut without_old = current
            .checked_sub(old_bytes)
            .ok_or_else(|| self.limit_error(usize::MAX))?;
        let target_capacity = if is_stack && required_capacity > self.stack_growth_exact_through {
            let preferred = values.capacity().max(1).saturating_mul(2).max(required_capacity);
            let preferred = if self.limit == usize::MAX {
                preferred
            } else {
                // Use the remaining budget for the last growth of a tight
                // non-power-of-two chain instead of growing one slot at a time.
                let affordable = self
                    .limit
                    .saturating_sub(without_old)
                    .checked_div(size_of::<T>())
                    .unwrap_or(usize::MAX);
                preferred.min(affordable).max(required_capacity)
            };
            if preferred
                .checked_mul(size_of::<T>())
                .filter(|bytes| *bytes <= isize::MAX as usize)
                .and_then(|bytes| without_old.checked_add(bytes))
                .is_some_and(|total| total <= self.limit)
            {
                preferred
            } else {
                required_capacity
            }
        } else {
            required_capacity
        };
        let requested_bytes = target_capacity
            .checked_mul(size_of::<T>())
            .ok_or_else(|| self.limit_error(usize::MAX))?;
        self.check_total_after_reclaim(&mut without_old, requested_bytes, stack.as_deref_mut())?;

        if values.try_reserve_exact(target_capacity - values.len()).is_err() {
            if self.limit == usize::MAX || target_capacity == required_capacity {
                return Err(JcsReserveError::Allocation);
            }
            // A speculative growth must not turn an exact-fit allocation into
            // an allocation failure.
            values
                .try_reserve_exact(required_capacity - values.len())
                .map_err(|_| JcsReserveError::Allocation)?;
        }
        let mut actual_bytes = values
            .capacity()
            .checked_mul(size_of::<T>())
            .ok_or_else(|| self.limit_error(usize::MAX))?;
        if is_stack
            && self.limit != usize::MAX
            && target_capacity > required_capacity
            && without_old
                .checked_add(actual_bytes)
                .is_none_or(|total| total > self.limit)
        {
            // Allocators may return more capacity than requested. Retry with
            // the exact requirement before rejecting a speculative growth.
            self.replace_stack(values, required_capacity)?;
            actual_bytes = self.stack_bytes;
        }
        let actual_total = self.check_total_after_reclaim(&mut without_old, actual_bytes, stack)?;

        if is_stack {
            self.stack_bytes = actual_bytes;
        } else {
            self.object_bytes = self
                .object_bytes
                .checked_sub(old_bytes)
                .and_then(|current| current.checked_add(actual_bytes))
                .ok_or_else(|| self.limit_error(usize::MAX))?;
        }
        self.peak_bytes = self.peak_bytes.max(actual_total);
        #[cfg(feature = "jcs-allocation-diagnostics")]
        if is_stack {
            self.stack_growth_count = self.stack_growth_count.saturating_add(1);
            self.stack_requested_bytes = self.stack_requested_bytes.saturating_add(requested_bytes);
            self.stack_max_actual_bytes = self.stack_max_actual_bytes.max(actual_bytes);
        } else {
            self.object_growth_count = self.object_growth_count.saturating_add(1);
            self.object_requested_bytes = self.object_requested_bytes.saturating_add(requested_bytes);
            self.object_max_actual_bytes = self.object_max_actual_bytes.max(actual_bytes);
        }
        Ok(())
    }

    /// Checks a replacement capacity, reclaiming unused stack slots only when
    /// the requested or actual total would exceed the limit.
    ///
    /// Returns the accepted total, or a limit/allocation error if reclaiming
    /// the stack cannot make the replacement fit. `without_old` is adjusted
    /// to reflect any successful reclamation.
    ///
    /// # Type Parameters
    ///
    /// - `T`: Element type stored by the optional stack whose spare capacity
    ///   may be reclaimed.
    ///
    /// # Parameters
    ///
    /// - `without_old`: Combined tracked bytes excluding the replaced
    ///   allocation; updated after successful stack reclamation.
    /// - `replacement_bytes`: Bytes requested for the replacement allocation.
    /// - `stack`: Optional traversal stack that may be reduced to its live
    ///   length to make the replacement fit.
    fn check_total_after_reclaim<T>(
        &mut self,
        without_old: &mut usize,
        replacement_bytes: usize,
        stack: Option<&mut Vec<T>>,
    ) -> Result<usize, JcsReserveError> {
        let mut total = without_old
            .checked_add(replacement_bytes)
            .ok_or_else(|| self.limit_error(usize::MAX))?;
        if total > self.limit {
            if let Some(stack) = stack {
                let old_stack_bytes = self.stack_bytes;
                let minimum_total = without_old
                    .checked_sub(old_stack_bytes)
                    .and_then(|bytes| {
                        stack
                            .len()
                            .checked_mul(size_of::<T>())
                            .and_then(|live| bytes.checked_add(live))
                    })
                    .and_then(|bytes| bytes.checked_add(replacement_bytes))
                    .ok_or_else(|| self.limit_error(usize::MAX))?;
                if minimum_total > self.limit {
                    return Err(self.limit_error(minimum_total));
                }
                self.replace_stack(stack, stack.len())?;
                // Future siblings may need progressively larger tables.
                // Reusing their reclaimed spare would copy these ancestors
                // again on each sibling, even when the old exact path fits.
                self.stack_growth_exact_through = self.stack_growth_exact_through.max(self.stack_max_required_capacity);
                *without_old = without_old
                    .checked_sub(old_stack_bytes)
                    .and_then(|bytes| bytes.checked_add(self.stack_bytes))
                    .ok_or_else(|| self.limit_error(usize::MAX))?;
                total = without_old
                    .checked_add(replacement_bytes)
                    .ok_or_else(|| self.limit_error(usize::MAX))?;
            }
            if total > self.limit {
                return Err(self.limit_error(total));
            }
        }
        Ok(total)
    }

    /// Replaces a stack with a fallibly allocated exact-capacity vector.
    ///
    /// Requested and actual replacement capacities must fit alongside live
    /// object tables or a limit error is returned. Allocation failure leaves
    /// the original stack intact. As with reserve, accounting excludes the
    /// old allocation; both allocations can briefly coexist physically.
    ///
    /// # Type Parameters
    ///
    /// - `T`: Element type stored by the traversal stack.
    ///
    /// # Parameters
    ///
    /// - `stack`: Traversal stack whose allocation is replaced.
    /// - `capacity`: Exact element capacity requested for the replacement.
    fn replace_stack<T>(&mut self, stack: &mut Vec<T>, capacity: usize) -> Result<(), JcsReserveError> {
        if capacity >= stack.capacity() {
            return Ok(());
        }
        let requested = capacity
            .checked_mul(size_of::<T>())
            .and_then(|bytes| self.object_bytes.checked_add(bytes))
            .ok_or_else(|| self.limit_error(usize::MAX))?;
        if requested > self.limit {
            return Err(self.limit_error(requested));
        }
        let mut replacement = Vec::new();
        replacement
            .try_reserve_exact(capacity)
            .map_err(|_| JcsReserveError::Allocation)?;
        let actual_bytes = replacement
            .capacity()
            .checked_mul(size_of::<T>())
            .ok_or_else(|| self.limit_error(usize::MAX))?;
        let actual = self
            .object_bytes
            .checked_add(actual_bytes)
            .ok_or_else(|| self.limit_error(usize::MAX))?;
        if actual > self.limit {
            return Err(self.limit_error(actual));
        }
        replacement.append(stack);
        *stack = replacement;
        self.stack_bytes = actual_bytes;
        #[cfg(feature = "jcs-allocation-diagnostics")]
        {
            self.stack_replacement_count = self.stack_replacement_count.saturating_add(1);
            self.stack_requested_bytes = self.stack_requested_bytes.saturating_add(capacity * size_of::<T>());
            self.stack_max_actual_bytes = self.stack_max_actual_bytes.max(actual_bytes);
        }
        Ok(())
    }

    /// Creates the limit error for a requested total byte count.
    ///
    /// # Parameters
    ///
    /// - `requested`: Combined byte capacity that triggered the limit error.
    ///
    /// # Returns
    ///
    /// A limit error containing the configured limit and requested total.
    #[inline]
    fn limit_error(&self, requested: usize) -> JcsReserveError {
        JcsReserveError::Limit(JcsWorkingMemoryError::new(self.limit, requested))
    }
}

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

    #[test]
    fn test_reserve_rejects_accounting_overflow_without_allocating() {
        for path in ["live-total", "untracked-capacity", "element-bytes", "requested-total"] {
            let mut memory = JcsWorkingMemory::new(usize::MAX);
            let mut values = Vec::<u64>::new();
            let additional = match path {
                "live-total" => {
                    memory.stack_bytes = usize::MAX;
                    memory.object_bytes = 1;
                    1
                }
                "untracked-capacity" => {
                    values = vec![0];
                    values.capacity() + 1
                }
                "element-bytes" => usize::MAX / std::mem::size_of::<u64>() + 1,
                _ => {
                    memory.stack_bytes = usize::MAX - 1;
                    1
                }
            };
            let capacity = values.capacity();
            let tracked = (memory.stack_bytes, memory.object_bytes, memory.peak_bytes());
            match memory.reserve_object_entries(&mut values, additional, &mut Vec::<u64>::new()) {
                Err(JcsReserveError::Limit(error)) => {
                    assert_eq!(error.limit(), usize::MAX);
                    assert_eq!(error.requested(), usize::MAX);
                }
                other => panic!("expected arithmetic rejection for {path}, got {other:?}"),
            }
            assert_eq!(values.capacity(), capacity, "{path} allocated before checking");
            assert_eq!((memory.stack_bytes, memory.object_bytes, memory.peak_bytes()), tracked);
        }
    }

    #[test]
    fn test_tracks_exact_capacity_boundary_and_rejects_one_byte_less() {
        let requested = std::mem::size_of::<u64>() * 2;
        let mut exact = JcsWorkingMemory::new(requested);
        let mut entries = Vec::<u64>::new();
        exact
            .reserve_object_entries(&mut entries, 2, &mut Vec::<u64>::new())
            .unwrap();
        assert!(entries.capacity() >= 2);
        assert!(exact.peak_bytes() >= requested);

        let mut short = JcsWorkingMemory::new(requested - 1);
        let mut entries = Vec::<u64>::new();
        match short.reserve_object_entries(&mut entries, 2, &mut Vec::<u64>::new()) {
            Err(JcsReserveError::Limit(error)) => {
                assert_eq!(error.limit(), requested - 1);
                assert!(error.requested() >= requested);
            }
            other => panic!("expected a working memory limit error, got {other:?}"),
        }
        assert!(entries.is_empty());
    }

    #[test]
    fn test_tracks_stack_capacity_and_keeps_it_after_a_no_growth_reserve() {
        let mut memory = JcsWorkingMemory::new(usize::MAX);
        let mut stack = Vec::<u64>::new();

        memory.reserve_stack(&mut stack, 2).unwrap();
        let expected = stack.capacity() * std::mem::size_of::<u64>();
        assert_eq!(memory.stack_bytes, expected);
        assert_eq!(memory.peak_bytes(), expected);

        memory.reserve_stack(&mut stack, 1).unwrap();
        assert_eq!(memory.stack_bytes, expected);
        assert_eq!(memory.peak_bytes(), expected);
    }

    #[test]
    fn test_unlimited_stack_growth_is_amortized() {
        let mut memory = JcsWorkingMemory::new(usize::MAX);
        let mut stack = Vec::<u64>::new();
        let mut growths = 0;
        for _ in 0..8192 {
            let old = stack.capacity();
            memory.reserve_stack(&mut stack, 1).unwrap();
            growths += usize::from(stack.capacity() != old);
            stack.push(0);
        }
        assert!(growths <= 15, "unexpected growth count: {growths}");
        assert_eq!(memory.stack_bytes, stack.capacity() * std::mem::size_of::<u64>());
    }

    #[test]
    fn test_finite_stack_growth_is_amortized() {
        for depth in [4097, 6000, 8192] {
            let mut memory = JcsWorkingMemory::new(depth * std::mem::size_of::<u64>());
            let mut stack = Vec::<u64>::new();
            let mut growths = 0;
            let mut requested_bytes = 0;
            for _ in 0..depth {
                let old = stack.capacity();
                memory
                    .reserve_stack(&mut stack, 1)
                    .expect("finite deep stack fits its exact budget");
                if stack.capacity() != old {
                    growths += 1;
                    requested_bytes += stack.capacity() * std::mem::size_of::<u64>();
                }
                stack.push(0);
            }
            assert!(growths <= 15, "unexpected finite growth count: {growths}");
            // A final budget-capped growth adds at most one full budget to
            // the preceding geometric series, which is below two budgets.
            assert!(requested_bytes <= 3 * memory.limit);
            assert_eq!(memory.stack_bytes, stack.capacity() * std::mem::size_of::<u64>());
            assert!(memory.peak_bytes() <= memory.limit);
        }
    }

    #[test]
    fn test_finite_stack_falls_back_to_exact_capacity_near_limit() {
        let mut memory = JcsWorkingMemory::new(13 * std::mem::size_of::<u64>());
        let mut stack = Vec::new();
        for value in 0_u64..13 {
            memory
                .reserve_stack(&mut stack, 1)
                .expect("exact capacity fits the remaining budget");
            stack.push(value);
        }
        assert_eq!(stack, (0_u64..13).collect::<Vec<_>>());
        assert_eq!(memory.stack_bytes, memory.limit);
        match memory.reserve_stack(&mut stack, 1) {
            Err(JcsReserveError::Limit(error)) => {
                assert_eq!(error.limit(), memory.limit);
                assert_eq!(error.requested(), 14 * std::mem::size_of::<u64>());
            }
            other => panic!("expected the next stack frame to exceed the budget, got {other:?}"),
        }
    }

    #[test]
    fn test_object_table_reclaims_stack_spare_without_losing_active_values() {
        let mut memory = JcsWorkingMemory::new(10 * std::mem::size_of::<u64>());
        let mut stack = Vec::new();
        for value in 0_u64..5 {
            memory
                .reserve_stack(&mut stack, 1)
                .expect("stack fits before the object table");
            stack.push(value);
        }
        let mut entries = Vec::<u64>::new();
        memory
            .reserve_object_entries(&mut entries, 4, &mut stack)
            .expect("reclaiming stack spare must leave room for the member table");
        assert_eq!(stack, (0_u64..5).collect::<Vec<_>>());
        assert_eq!(memory.stack_bytes, stack.capacity() * std::mem::size_of::<u64>());
        memory
            .reserve_stack(&mut stack, 1)
            .expect("the object frame fits after its table");
        stack.push(5);
        assert_eq!(memory.stack_bytes + memory.object_bytes, memory.limit);
        assert!(memory.peak_bytes() <= memory.limit);
        #[cfg(feature = "jcs-allocation-diagnostics")]
        assert_eq!(memory.stack_replacement_count, 1);
    }

    #[test]
    fn test_growing_sibling_tables_do_not_repeatedly_copy_deep_ancestors() {
        let depth = 8192;
        let widths = 128;
        let mut memory = JcsWorkingMemory::new(
            (depth + 1) * std::mem::size_of::<[u64; 4]>() + widths * std::mem::size_of::<[u64; 3]>(),
        );
        let mut stack = Vec::new();
        for _ in 0..depth {
            memory
                .reserve_stack(&mut stack, 1)
                .expect("ancestor stack fits the exact budget");
            stack.push([0_u64; 4]);
        }
        let mut replacements = 0;
        for width in 1..=widths {
            let mut entries = Vec::<[u64; 3]>::new();
            let old_capacity = stack.capacity();
            memory
                .reserve_object_entries(&mut entries, width, &mut stack)
                .expect("all sibling tables fit alongside the exact stack high-water capacity");
            replacements += usize::from(stack.capacity() < old_capacity);
            memory.reserve_stack(&mut stack, 1).expect("the sibling's frame fits");
            stack.push([0_u64; 4]);
            stack.pop();
            memory
                .release_object_entries(&entries)
                .expect("release the completed sibling table");
        }
        assert!(replacements <= 1, "repeated ancestor copies: {replacements}");
        assert!(memory.peak_bytes() <= memory.limit);
    }

    #[test]
    fn test_releases_object_capacity_before_a_later_object_is_reserved() {
        let mut memory = JcsWorkingMemory::new(usize::MAX);
        let mut stack = Vec::<u64>::new();
        memory.reserve_stack(&mut stack, 1).unwrap();

        let mut first_entries = Vec::<u64>::new();
        memory
            .reserve_object_entries(&mut first_entries, 2, &mut stack)
            .unwrap();
        let total = memory.stack_bytes + memory.object_bytes;
        memory.limit = total;
        memory.release_object_entries(&first_entries).unwrap();
        drop(first_entries);

        let mut second_entries = Vec::<u64>::new();
        memory
            .reserve_object_entries(&mut second_entries, 2, &mut stack)
            .unwrap();
        assert_eq!(
            memory.object_bytes,
            second_entries.capacity() * std::mem::size_of::<u64>()
        );
        assert!(memory.stack_bytes + memory.object_bytes <= memory.limit);
    }

    #[test]
    fn test_reports_allocation_and_accounting_failures() {
        let impossible_capacity = isize::MAX as usize + 1;

        let mut stack_memory = JcsWorkingMemory::new(usize::MAX);
        let mut stack = Vec::<u8>::new();
        assert!(matches!(
            stack_memory.reserve_stack(&mut stack, impossible_capacity),
            Err(JcsReserveError::Allocation),
        ));
        assert!(stack.is_empty());

        let mut object_memory = JcsWorkingMemory::new(usize::MAX);
        let mut entries = Vec::<u8>::new();
        assert!(matches!(
            object_memory.reserve_object_entries(&mut entries, impossible_capacity, &mut Vec::<u8>::new()),
            Err(JcsReserveError::Allocation),
        ));
        assert!(entries.is_empty());

        let mut populated_stack = vec![0_u8];
        let mut memory = JcsWorkingMemory::new(usize::MAX);
        match memory.reserve_stack(&mut populated_stack, usize::MAX) {
            Err(JcsReserveError::Limit(error)) => {
                assert_eq!(error.limit(), usize::MAX);
                assert_eq!(error.requested(), usize::MAX);
            }
            other => panic!("expected a stack capacity overflow, got {other:?}"),
        }

        let untracked_entries = vec![0_u8];
        match memory.release_object_entries(&untracked_entries) {
            Err(JcsReserveError::Limit(error)) => {
                assert_eq!(error.limit(), usize::MAX);
                assert_eq!(error.requested(), usize::MAX);
            }
            other => panic!("expected an untracked capacity error, got {other:?}"),
        }
    }

    #[cfg(feature = "jcs-allocation-diagnostics")]
    #[test]
    fn test_diagnostic_counts_successful_growth_by_collection() {
        let mut memory = JcsWorkingMemory::new(usize::MAX);
        let mut stack = Vec::<u64>::new();
        let mut entries = Vec::<(usize, usize)>::new();

        memory.reserve_stack(&mut stack, 2).unwrap();
        memory.reserve_stack(&mut stack, 1).unwrap();
        memory
            .reserve_object_entries(&mut entries, 2, &mut Vec::<u64>::new())
            .unwrap();

        assert_eq!(memory.stack_growth_count, 1);
        assert_eq!(memory.stack_requested_bytes, 2 * std::mem::size_of::<u64>());
        assert_eq!(
            memory.stack_max_actual_bytes,
            stack.capacity() * std::mem::size_of::<u64>()
        );
        assert_eq!(memory.object_growth_count, 1);
        assert_eq!(memory.object_requested_bytes, 2 * std::mem::size_of::<(usize, usize)>());
        assert_eq!(
            memory.object_max_actual_bytes,
            entries.capacity() * std::mem::size_of::<(usize, usize)>(),
        );
    }

    #[cfg(feature = "jcs-allocation-diagnostics")]
    #[test]
    fn test_diagnoses_stack_growth_after_reclaiming_wide_table() {
        use std::mem::size_of;

        for depth in [128, 512, 2048] {
            let mut stack = Vec::<[u64; 4]>::new();
            let mut memory = JcsWorkingMemory::new(depth * size_of::<[u64; 4]>() + 1024);
            for _ in 0..9 {
                memory.reserve_stack(&mut stack, 1).unwrap();
                stack.push([0; 4]);
            }
            stack.pop();
            assert!(stack.capacity() > stack.len());

            let budget = memory.limit;
            let old_stack_bytes = stack.capacity() * size_of::<[u64; 4]>();
            let live_stack_bytes = stack.len() * size_of::<[u64; 4]>();
            let table_element_bytes = size_of::<[u64; 3]>();
            let table_width = (budget - old_stack_bytes) / table_element_bytes + 1;
            let mut entries = Vec::<[u64; 3]>::new();
            memory
                .reserve_object_entries(&mut entries, table_width, &mut stack)
                .expect("wide table fits after reclaiming unused stack capacity");
            assert!(stack.capacity() < 128, "stack capacity was not reclaimed");
            assert!(memory.stack_bytes >= live_stack_bytes);
            let peak_with_table = memory.peak_bytes();
            memory.release_object_entries(&entries).unwrap();
            drop(entries);
            let replacements_for_reclaim = memory.stack_replacement_count;

            let (growth_snapshot, replacement_snapshot, requested_snapshot) = (
                memory.stack_growth_count,
                memory.stack_replacement_count,
                memory.stack_requested_bytes,
            );
            let initial_stack_capacity = stack.capacity();
            let initial_stack_len = stack.len();
            while stack.len() < depth {
                memory.reserve_stack(&mut stack, 1).unwrap();
                stack.push([0; 4]);
            }
            let post_growths = memory.stack_growth_count - growth_snapshot;
            let post_replacements = memory.stack_replacement_count - replacement_snapshot;
            let post_requested = memory.stack_requested_bytes - requested_snapshot;
            let peak_after_deep_branch = memory.peak_bytes();
            assert!(memory.stack_bytes + memory.object_bytes <= budget);

            let mut control_memory = JcsWorkingMemory::new(budget);
            let mut control_stack = Vec::<[u64; 4]>::new();
            for _ in 0..8 {
                control_memory.reserve_stack(&mut control_stack, 1).unwrap();
                control_stack.push([0; 4]);
            }
            assert_eq!(control_stack.capacity(), initial_stack_capacity);
            assert_eq!(control_stack.len(), initial_stack_len);
            let control_growth_snapshot = control_memory.stack_growth_count;
            while control_stack.len() < depth {
                control_memory.reserve_stack(&mut control_stack, 1).unwrap();
                control_stack.push([0; 4]);
            }
            let control_growths = control_memory.stack_growth_count - control_growth_snapshot;
            assert!(replacements_for_reclaim >= 1);
            assert!(
                post_growths <= 2 * depth.ilog2() as usize + 4,
                "post-reclaim stack growth should remain geometric: depth={depth}, growths={post_growths}",
            );
            println!(
                "JCS_STACK_RECLAIM depth={depth} budget={budget} live_stack_bytes={live_stack_bytes} peak_with_table={peak_with_table} peak_after_deep_branch={peak_after_deep_branch} reclaim_replacements={replacements_for_reclaim} post_growths={post_growths} post_replacements={post_replacements} post_requested_bytes={post_requested} control_growths={control_growths} control_requested_bytes={}",
                control_memory.stack_requested_bytes,
            );
        }
    }
}