Skip to main content

sui_bytecode/
fallback.rs

1//! Fallback accounting — and the strict latch that makes a laundered answer
2//! impossible to mistake for a computed one.
3//!
4//! # The problem this exists to solve
5//!
6//! The VM does not evaluate alone. It delegates to the tree-walker at **three**
7//! independent granularities, and until now none of them was observable in
8//! practice:
9//!
10//! | layer | where | granularity |
11//! |---|---|---|
12//! | [`Layer::Builtin`] | `builtins.rs`, the bridge call | one builtin call |
13//! | [`Layer::ImportedFile`] | `vm.rs`, `import_file` | one imported file |
14//! | [`Layer::WholeExpression`] | the CLI's VM arm | the entire expression |
15//!
16//! The consequence is that a test comparing "the VM" against the tree-walker
17//! can be answered *by the tree-walker on both sides*. That is not theoretical:
18//! `tests/vm_cli.rs` (36 cases) and `tests/vm_capabilities.rs` (23) go through
19//! the CLI, whose VM arm falls back on any error — so **a VM failing 100% of
20//! those expressions passed 36/36**. A green run meant "the VM did not produce
21//! a *different* answer", never "the VM computed this".
22//!
23//! A counter for the middle layer already existed (`vm_fallback_count()`) and
24//! **nothing in the repo ever read it**.
25//!
26//! # What strict mode does, and what it deliberately does not
27//!
28//! `SUI_VM_STRICT=1` makes [`Layer::ImportedFile`] and
29//! [`Layer::WholeExpression`] **hard errors**. Both mean the same thing — the
30//! VM could not do its job and the walker covered for it — and that is exactly
31//! what a measurement must not silently absorb.
32//!
33//! [`Layer::Builtin`] is counted but **never fatal, at any setting**. Bridging
34//! a builtin is the VM's *architecture*, not a failure: it has no native
35//! `getEnv`, `match`, `split`, `fromTOML`, `genericClosure`, `readDir` or
36//! `hashFile`, and it is not supposed to. Making that arm fatal would leave
37//! strict mode unable to evaluate anything at all, which is a strict mode
38//! nobody can use. A caller that wants full purity asserts
39//! `count(Layer::Builtin) == 0` for itself.
40//!
41//! Being explicit about that asymmetry is the point: a latch that conflates
42//! "the VM delegated by design" with "the VM failed" would produce a red that
43//! nobody could act on, and reds nobody can act on get switched off.
44
45use std::sync::atomic::{AtomicU64, Ordering};
46use std::sync::OnceLock;
47
48/// Which delegation boundary was crossed.
49#[derive(Clone, Copy, Debug, PartialEq, Eq)]
50pub enum Layer {
51    /// A builtin with no native VM implementation went to the tree-walker.
52    /// Architectural — counted, never fatal.
53    Builtin,
54    /// An imported file failed to compile or to run, and the tree-walker
55    /// evaluated it instead. Fatal under strict.
56    ImportedFile,
57    /// The whole top-level expression failed and was re-run on the
58    /// tree-walker. Fatal under strict.
59    WholeExpression,
60}
61
62impl Layer {
63    #[must_use]
64    pub fn name(self) -> &'static str {
65        match self {
66            Layer::Builtin => "builtin",
67            Layer::ImportedFile => "imported-file",
68            Layer::WholeExpression => "whole-expression",
69        }
70    }
71
72    /// Whether strict mode refuses this layer.
73    #[must_use]
74    pub fn is_fatal_under_strict(self) -> bool {
75        match self {
76            Layer::Builtin => false,
77            Layer::ImportedFile | Layer::WholeExpression => true,
78        }
79    }
80
81    /// Every layer, so a caller can report totals without hand-listing —
82    /// and so a new layer cannot be added without appearing in the report.
83    pub const ALL: &'static [Layer] = &[
84        Layer::Builtin,
85        Layer::ImportedFile,
86        Layer::WholeExpression,
87    ];
88}
89
90static BUILTIN: AtomicU64 = AtomicU64::new(0);
91static IMPORTED_FILE: AtomicU64 = AtomicU64::new(0);
92static WHOLE_EXPRESSION: AtomicU64 = AtomicU64::new(0);
93
94fn cell(layer: Layer) -> &'static AtomicU64 {
95    match layer {
96        Layer::Builtin => &BUILTIN,
97        Layer::ImportedFile => &IMPORTED_FILE,
98        Layer::WholeExpression => &WHOLE_EXPRESSION,
99    }
100}
101
102/// `true` when `SUI_VM_STRICT=1`.
103///
104/// Latched once, like the other env gates in this workspace, so a mid-run
105/// change cannot make one half of an evaluation strict and the other half not.
106#[must_use]
107pub fn strict() -> bool {
108    static ON: OnceLock<bool> = OnceLock::new();
109    *ON.get_or_init(|| std::env::var("SUI_VM_STRICT").as_deref() == Ok("1"))
110}
111
112/// Record that `layer` was crossed.
113///
114/// Returns `Err` with an operator-facing message when strict mode refuses this
115/// layer; the caller must propagate rather than fall back. Always counts,
116/// strict or not — the counts are useful on their own, and a caller that wants
117/// to know "did the VM really do this" reads them.
118///
119/// # Errors
120///
121/// When [`strict`] is on and the layer [`Layer::is_fatal_under_strict`].
122pub fn record(layer: Layer, detail: &str) -> Result<(), String> {
123    cell(layer).fetch_add(1, Ordering::Relaxed);
124    if strict() && layer.is_fatal_under_strict() {
125        return Err(format!(
126            "SUI_VM_STRICT: refusing to fall back to the tree-walker at the \
127             {} boundary: {detail}. Strict mode exists so a measurement cannot \
128             silently become the walker's answer — if you want the fallback, \
129             unset SUI_VM_STRICT; if you want the VM to handle this, that is \
130             the bug.",
131            layer.name()
132        ));
133    }
134    Ok(())
135}
136
137/// How many times `layer` was crossed this process.
138#[must_use]
139pub fn count(layer: Layer) -> u64 {
140    cell(layer).load(Ordering::Relaxed)
141}
142
143/// Total across every layer.
144#[must_use]
145pub fn total() -> u64 {
146    Layer::ALL.iter().map(|l| count(*l)).sum()
147}
148
149/// One line per layer, for a diagnostic dump.
150#[must_use]
151pub fn report() -> String {
152    Layer::ALL
153        .iter()
154        .map(|l| format!("{}={}", l.name(), count(*l)))
155        .collect::<Vec<_>>()
156        .join(" ")
157}
158
159/// Zero every counter. Test-support only.
160///
161/// The counters are process-global `AtomicU64`s, so a test that asserts on a
162/// count must reset first AND must not run concurrently with another test that
163/// evaluates. Prefer asserting a *delta* you captured yourself.
164pub fn reset() {
165    for l in Layer::ALL {
166        cell(*l).store(0, Ordering::Relaxed);
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    #[test]
175    fn builtin_bridging_is_never_fatal() {
176        // Holds regardless of the latch: bridging a builtin is architecture,
177        // not failure. If this ever starts erroring, strict mode becomes
178        // unusable rather than stricter.
179        assert!(!Layer::Builtin.is_fatal_under_strict());
180        assert!(record(Layer::Builtin, "getEnv").is_ok());
181    }
182
183    #[test]
184    fn the_two_failure_layers_are_fatal_under_strict() {
185        assert!(Layer::ImportedFile.is_fatal_under_strict());
186        assert!(Layer::WholeExpression.is_fatal_under_strict());
187    }
188
189    #[test]
190    fn counting_happens_whether_or_not_strict_is_on() {
191        // The count is the instrument; the latch only decides whether crossing
192        // is fatal. A caller reading counts must work with strict off.
193        let before = count(Layer::Builtin);
194        let _ = record(Layer::Builtin, "probe");
195        assert_eq!(count(Layer::Builtin), before + 1);
196    }
197
198    #[test]
199    fn report_names_every_layer() {
200        // Anti-vacuity for the layer SET: adding a variant without adding it
201        // to ALL would silently drop it from every report and total.
202        let r = report();
203        for l in Layer::ALL {
204            assert!(r.contains(l.name()), "report() omits {}", l.name());
205        }
206        assert_eq!(Layer::ALL.len(), 3, "a layer was added or removed");
207    }
208}