Skip to main content

bop/
memory.rs

1//! Memory tracking for Bop script execution.
2//!
3//! By default, uses thread-local counters so tracking is
4//! zero-cost and perfectly isolated between concurrent executions.
5//!
6//! With `no_std`, uses global statics. This is safe for single-threaded
7//! environments (e.g., wasm) but is NOT thread-safe.
8//!
9//! Tracking happens at the Value layer: Clone tracks new allocations, Drop tracks
10//! frees. The evaluator only needs to call `bop_memory_init()` at the start and
11//! check `bop_memory_exceeded()` in `tick()`.
12
13// ─── std: thread-local storage ──────────────────────────────────────────────
14
15#[cfg(not(feature = "no_std"))]
16mod imp {
17    use std::cell::Cell;
18
19    thread_local! {
20        static USED: Cell<usize> = const { Cell::new(0) };
21        static LIMIT: Cell<usize> = const { Cell::new(usize::MAX) };
22    }
23
24    pub fn init(limit: usize) {
25        USED.set(0);
26        LIMIT.set(limit);
27    }
28
29    pub fn alloc(bytes: usize) {
30        USED.with(|u| u.set(u.get().saturating_add(bytes)));
31    }
32
33    pub fn dealloc(bytes: usize) {
34        USED.with(|u| u.set(u.get().saturating_sub(bytes)));
35    }
36
37    pub fn exceeded() -> bool {
38        USED.with(|u| LIMIT.with(|l| u.get() > l.get()))
39    }
40
41    pub fn would_exceed(bytes: usize) -> bool {
42        USED.with(|u| LIMIT.with(|l| u.get().saturating_add(bytes) > l.get()))
43    }
44}
45
46// ─── no-std: global statics (single-threaded only) ──────────────────────────
47
48#[cfg(feature = "no_std")]
49mod imp {
50    use core::cell::Cell;
51
52    // Safety: bop is single-threaded in no-std mode (e.g., wasm).
53    // SyncCell lets us put Cell in a static.
54    struct SyncCell(Cell<usize>);
55    unsafe impl Sync for SyncCell {}
56
57    static USED: SyncCell = SyncCell(Cell::new(0));
58    static LIMIT: SyncCell = SyncCell(Cell::new(usize::MAX));
59
60    pub fn init(limit: usize) {
61        USED.0.set(0);
62        LIMIT.0.set(limit);
63    }
64
65    pub fn alloc(bytes: usize) {
66        USED.0.set(USED.0.get().saturating_add(bytes));
67    }
68
69    pub fn dealloc(bytes: usize) {
70        USED.0.set(USED.0.get().saturating_sub(bytes));
71    }
72
73    pub fn exceeded() -> bool {
74        USED.0.get() > LIMIT.0.get()
75    }
76
77    pub fn would_exceed(bytes: usize) -> bool {
78        USED.0.get().saturating_add(bytes) > LIMIT.0.get()
79    }
80}
81
82// ─── Public API (delegates to the active impl) ─────────────────────────────
83
84/// Reset the counter and set the limit for this simulation.
85pub fn bop_memory_init(limit: usize) {
86    imp::init(limit);
87}
88
89/// Track a new heap allocation. Does not check the limit.
90/// Called by Value's Clone impl and constructor helpers.
91pub fn bop_alloc(bytes: usize) {
92    imp::alloc(bytes);
93}
94
95/// Track a deallocation. Called by Value's Drop impl.
96pub fn bop_dealloc(bytes: usize) {
97    imp::dealloc(bytes);
98}
99
100/// Returns true if current usage exceeds the limit.
101/// Checked in `tick()` to catch allocations from clones.
102pub fn bop_memory_exceeded() -> bool {
103    imp::exceeded()
104}
105
106/// Pre-flight check: would allocating `bytes` more exceed the limit?
107/// Does NOT modify the counter. Use before creating large values
108/// (string repeat, range) to avoid allocating memory we'll immediately reject.
109pub fn bop_would_exceed(bytes: usize) -> bool {
110    imp::would_exceed(bytes)
111}