gap315_dside_test/gap315_dside_test.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! GAP-315 — the genuine (d)-SIDE FORWARD TEST. MEASUREMENT ONLY, not a gate.
3//!
4//! ⚠️ PREDICTIONS AND THE CORRECT ANSWER WERE REGISTERED BEFORE THIS RAN.
5//!
6//! SITE: `Storage::with_bundle`. Chosen because it satisfies every clause of the
7//! spec the earlier probes could not:
8//! - returns `Result<Self>`, and the error path is LIVE (not theoretical):
9//! `validate_bundle` rejects an empty bundle and a slot-0 dtype mismatch.
10//! Arm [ctl-err] below exercises that, so a silent `Ok` elsewhere cannot be
11//! explained by "nothing in this function can fail".
12//! - nothing on that error path checks the asserted quantity: `validate_bundle`
13//! validates the NEW bundle against the dtype and never looks at the existing one.
14//! - no second mechanism forces the outcome — no `as usize` wrap, no missing
15//! error variant, no bounds check in play.
16//!
17//! 🔴 THE CORRECT ANSWER, NAMED FROM THE CONTRACT BEFORE PROBING:
18//! the function's own doc says "panics in debug mode if a bundle is already
19//! attached (RE-BUNDLING IS A CONTRACT BUG)". So for a re-bundle the callee owes
20//! a REJECTION — or at minimum must not silently discard the first bundle.
21//! PREDICTED release behaviour: `Ok` with the SECOND bundle installed and the
22//! FIRST silently gone => (d).
23//!
24//! ⚠️ WHY THIS IS NOT THE conv CASE: the violation and the harm are THE SAME ACT.
25//! Re-bundling is the contract bug and its immediate consequence is that the
26//! caller's first bundle is destroyed. No second caller error is needed to reach
27//! the harm — which is the test conv failed and `advertise` passed.
28//!
29//! Run both arms per the other GAP-315 probes:
30//! arm A (default) debug_assertions ON guard present
31//! arm B RUSTFLAGS="-C target-cpu=native -C debug-assertions=off" guard ABSENT
32
33use fuel_cpu_backend::byte_storage::CpuStorageBytes;
34use fuel_ir::storage::{OutputViewSpec, compose_bundle};
35use fuel_ir::{DType, Shape};
36use fuel_memory::{BackendStorage, Storage};
37use std::sync::Arc;
38
39enum Outcome {
40 /// Rejected with a typed error — a guard that survives release.
41 Err(String),
42 Panic(String),
43 /// Accepted. `slots` is how many views the resulting Storage carries, which
44 /// is how we tell WHICH bundle survived. This is the (d) shape.
45 Ok {
46 slots: usize,
47 },
48}
49
50// A hand-written `Debug` (not `derive`) so the report below really READS every
51// field; a derived `Debug` does not count as a read for `dead_code`.
52impl std::fmt::Debug for Outcome {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 match self {
55 Outcome::Err(s) => write!(f, "Err({s:?})"),
56 Outcome::Panic(s) => write!(f, "Panic({s:?})"),
57 Outcome::Ok { slots } => write!(f, "Ok {{ slots: {slots:?} }}"),
58 }
59 }
60}
61
62fn specs_of(n: usize) -> Vec<OutputViewSpec> {
63 (0..n)
64 .map(|i| OutputViewSpec::contiguous(DType::F32, Shape::from_dims(&[4 + i])))
65 .collect()
66}
67
68/// Build a Storage carrying a bundle of `n` slots, all F32.
69fn bundled(n: usize) -> Storage {
70 let (total_bytes, views) = compose_bundle(&specs_of(n)).expect("compose_bundle");
71 Storage::new_bundled(
72 BackendStorage::Cpu(CpuStorageBytes::from_zero_bytes(total_bytes)),
73 DType::F32,
74 Arc::from(views.into_boxed_slice()),
75 )
76 .expect("new_bundled")
77}
78
79/// A bundle of `n` F32 slots, to hand to `with_bundle`.
80fn bundle_of(n: usize) -> Arc<[fuel_ir::storage::OutputView]> {
81 let (_bytes, views) = compose_bundle(&specs_of(n)).expect("compose_bundle");
82 Arc::from(views.into_boxed_slice())
83}
84
85fn probe<F: FnOnce() -> fuel_ir::Result<Storage> + std::panic::UnwindSafe>(f: F) -> Outcome {
86 let prev = std::panic::take_hook();
87 std::panic::set_hook(Box::new(|_| {}));
88 let r = std::panic::catch_unwind(f);
89 std::panic::set_hook(prev);
90 match r {
91 Err(p) => {
92 let msg = p
93 .downcast_ref::<String>()
94 .cloned()
95 .or_else(|| p.downcast_ref::<&str>().map(|s| (*s).to_string()))
96 .unwrap_or_else(|| "<non-string panic>".into());
97 Outcome::Panic(msg.lines().next().unwrap_or("").to_string())
98 }
99 Ok(Err(e)) => Outcome::Err(format!("{e}").lines().next().unwrap_or("").to_string()),
100 Ok(Ok(s)) => Outcome::Ok {
101 slots: s.bundle().map(|b| b.len()).unwrap_or(0),
102 },
103 }
104}
105
106fn main() {
107 println!(
108 "\n=== GAP-315 (d)-SIDE FORWARD TEST: Storage::with_bundle ===\n\
109 debug_assertions = {} ({})",
110 cfg!(debug_assertions),
111 if cfg!(debug_assertions) {
112 "ARM A - guard PRESENT (positive control)"
113 } else {
114 "ARM B - guard ABSENT (what the shipped artifact does)"
115 }
116 );
117
118 // CONTROL: the legitimate use — attach a bundle to an UNBUNDLED Storage.
119 // Must behave identically in both arms.
120 println!(
121 " [control] unbundled + with_bundle(2) -> {:?}",
122 probe(|| {
123 let s = Storage::new(
124 BackendStorage::Cpu(CpuStorageBytes::from_zero_bytes(64)),
125 DType::F32,
126 );
127 s.with_bundle(bundle_of(2))
128 })
129 );
130
131 // IN-PROBE CONTROL: the error path is LIVE. An empty bundle must be REJECTED
132 // in BOTH arms. Without this, a silent Ok below could be explained away as
133 // "nothing in this function can fail".
134 println!(
135 " [ctl-err] unbundled + EMPTY bundle -> {:?}",
136 probe(|| {
137 let s = Storage::new(
138 BackendStorage::Cpu(CpuStorageBytes::from_zero_bytes(64)),
139 DType::F32,
140 );
141 s.with_bundle(Arc::from(Vec::new().into_boxed_slice()))
142 })
143 );
144
145 // 🔴 THE PREDICTION: RE-BUNDLE. Storage already carries 3 slots; attach 2 more.
146 // Contract says re-bundling is a bug, so the callee owes a rejection.
147 // PREDICT arm B: Ok { slots: 2 } — the FIRST bundle (3 slots) silently gone.
148 println!(
149 " [rebundle] bundled(3) + with_bundle(2) -> {:?}",
150 probe(|| bundled(3).with_bundle(bundle_of(2)))
151 );
152
153 // Same violation, sizes swapped, so "slots" cannot be read as a coincidence.
154 println!(
155 " [rebundle2] bundled(2) + with_bundle(5) -> {:?}",
156 probe(|| bundled(2).with_bundle(bundle_of(5)))
157 );
158
159 println!(" (measurement only - asserts nothing)");
160}