1use core::cell::RefCell;
18
19use alloc::borrow::ToOwned;
20use alloc::string::String;
21use alloc::vec::Vec;
22
23use brink_format::Value;
24
25use crate::story::{ExternalFnHandler, ExternalResult};
26
27pub const RECORDING_CAP: usize = 16_384;
31
32#[derive(Clone, Debug, PartialEq)]
35pub struct RecordedExternal {
36 pub name: String,
38 pub args: Vec<Value>,
40 pub result: Value,
42}
43
44#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
46pub enum ReplayMode {
47 #[default]
51 Recorded,
52 Live,
56}
57
58#[derive(Clone, Debug, Default, PartialEq)]
61pub struct ReplayRecorder {
62 log: Vec<RecordedExternal>,
63 cursor: usize,
64 diverged: bool,
65}
66
67impl ReplayRecorder {
68 #[must_use]
70 pub fn new() -> Self {
71 Self::default()
72 }
73
74 pub fn record(&mut self, name: &str, args: &[Value], result: &Value) {
77 if self.log.len() >= RECORDING_CAP {
78 return;
79 }
80 self.log.push(RecordedExternal {
81 name: name.to_owned(),
82 args: args.to_vec(),
83 result: result.clone(),
84 });
85 }
86
87 pub fn take_recorded(&mut self, name: &str, args: &[Value]) -> Option<Value> {
93 if self.diverged {
94 return None;
95 }
96 match self.log.get(self.cursor) {
97 Some(entry) if entry.name == name && entry.args.as_slice() == args => {
98 self.cursor += 1;
99 Some(entry.result.clone())
100 }
101 _ => {
102 self.diverged = true;
103 None
104 }
105 }
106 }
107
108 pub fn reset_cursor(&mut self) {
111 self.cursor = 0;
112 self.diverged = false;
113 }
114
115 #[must_use]
117 pub fn len(&self) -> usize {
118 self.log.len()
119 }
120
121 #[must_use]
123 pub fn is_empty(&self) -> bool {
124 self.log.is_empty()
125 }
126}
127
128pub struct RecordingHandler<'a, H: ExternalFnHandler + ?Sized> {
137 inner: &'a H,
138 recorder: RefCell<&'a mut ReplayRecorder>,
139}
140
141impl<'a, H: ExternalFnHandler + ?Sized> RecordingHandler<'a, H> {
142 pub fn new(inner: &'a H, recorder: &'a mut ReplayRecorder) -> Self {
144 Self {
145 inner,
146 recorder: RefCell::new(recorder),
147 }
148 }
149}
150
151impl<H: ExternalFnHandler + ?Sized> ExternalFnHandler for RecordingHandler<'_, H> {
152 fn call(&self, name: &str, args: &[Value]) -> ExternalResult {
153 let result = self.inner.call(name, args);
154 if let ExternalResult::Resolved(value) = &result {
155 self.recorder.borrow_mut().record(name, args, value);
156 }
157 result
158 }
159}
160
161pub struct ReplayHandler<'a> {
171 recorder: RefCell<&'a mut ReplayRecorder>,
172}
173
174impl<'a> ReplayHandler<'a> {
175 pub fn new(recorder: &'a mut ReplayRecorder) -> Self {
178 recorder.reset_cursor();
179 Self {
180 recorder: RefCell::new(recorder),
181 }
182 }
183}
184
185impl ExternalFnHandler for ReplayHandler<'_> {
186 fn call(&self, name: &str, args: &[Value]) -> ExternalResult {
187 match self.recorder.borrow_mut().take_recorded(name, args) {
188 Some(value) => ExternalResult::Resolved(value),
189 None => ExternalResult::Fallback,
190 }
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197
198 fn args(xs: &[i32]) -> Vec<Value> {
199 xs.iter().map(|&x| Value::Int(x)).collect()
200 }
201
202 #[test]
203 fn records_and_replays_in_order() {
204 let mut r = ReplayRecorder::new();
205 r.record("get_switch", &args(&[1]), &Value::Bool(true));
206 r.record("get_var", &args(&[2]), &Value::Int(42));
207 assert_eq!(r.len(), 2);
208
209 assert_eq!(
210 r.take_recorded("get_switch", &args(&[1])),
211 Some(Value::Bool(true))
212 );
213 assert_eq!(
214 r.take_recorded("get_var", &args(&[2])),
215 Some(Value::Int(42))
216 );
217 assert_eq!(r.take_recorded("get_var", &args(&[2])), None);
219 }
220
221 #[test]
222 fn diverges_on_mismatch_and_stays_diverged() {
223 let mut r = ReplayRecorder::new();
224 r.record("a", &args(&[1]), &Value::Int(1));
225 r.record("b", &args(&[2]), &Value::Int(2));
226 assert_eq!(r.take_recorded("x", &args(&[1])), None);
227 assert_eq!(r.take_recorded("a", &args(&[1])), None);
229 }
230
231 #[test]
232 fn arg_mismatch_diverges() {
233 let mut r = ReplayRecorder::new();
234 r.record("get_switch", &args(&[1]), &Value::Bool(true));
235 assert_eq!(r.take_recorded("get_switch", &args(&[2])), None);
236 }
237
238 #[test]
239 fn reset_cursor_replays_again() {
240 let mut r = ReplayRecorder::new();
241 r.record("a", &args(&[1]), &Value::Int(7));
242 assert_eq!(r.take_recorded("a", &args(&[1])), Some(Value::Int(7)));
243 r.reset_cursor();
244 assert_eq!(r.take_recorded("a", &args(&[1])), Some(Value::Int(7)));
245 }
246
247 #[test]
248 fn cap_drops_beyond_limit() {
249 let mut r = ReplayRecorder::new();
250 for _ in 0..RECORDING_CAP + 10 {
251 r.record("a", &[], &Value::Null);
252 }
253 assert_eq!(r.len(), RECORDING_CAP);
254 }
255
256 struct Stub(Vec<(&'static str, Value)>);
258 impl ExternalFnHandler for Stub {
259 fn call(&self, name: &str, _args: &[Value]) -> ExternalResult {
260 self.0
261 .iter()
262 .find(|(n, _)| *n == name)
263 .map_or(ExternalResult::Fallback, |(_, v)| {
264 ExternalResult::Resolved(v.clone())
265 })
266 }
267 }
268
269 #[test]
270 fn recording_captures_resolved_passes_through_fallback() {
271 let mut rec = ReplayRecorder::new();
272 let inner = Stub(vec![("get", Value::Int(5))]);
273 {
274 let h = RecordingHandler::new(&inner, &mut rec);
275 assert!(matches!(h.call("get", &[]), ExternalResult::Resolved(_)));
276 assert!(matches!(h.call("nope", &[]), ExternalResult::Fallback));
277 }
278 assert_eq!(rec.len(), 1);
279 }
280
281 #[test]
282 fn replay_returns_recorded_then_fallback() {
283 let mut rec = ReplayRecorder::new();
284 rec.record("get", &[], &Value::Int(5));
285 let h = ReplayHandler::new(&mut rec);
286 assert!(matches!(
287 h.call("get", &[]),
288 ExternalResult::Resolved(Value::Int(5))
289 ));
290 assert!(matches!(h.call("get", &[]), ExternalResult::Fallback));
291 }
292
293 #[test]
294 fn record_then_replay_roundtrip() {
295 let mut rec = ReplayRecorder::new();
296 let inner = Stub(vec![("a", Value::Int(1)), ("b", Value::Bool(true))]);
297 {
298 let h = RecordingHandler::new(&inner, &mut rec);
299 let _ = h.call("a", &[]);
300 let _ = h.call("b", &[]);
301 }
302 let h = ReplayHandler::new(&mut rec);
303 assert!(matches!(
304 h.call("a", &[]),
305 ExternalResult::Resolved(Value::Int(1))
306 ));
307 assert!(matches!(
308 h.call("b", &[]),
309 ExternalResult::Resolved(Value::Bool(true))
310 ));
311 }
312
313 #[test]
314 fn replay_diverges_to_fallback_on_mismatch() {
315 let mut rec = ReplayRecorder::new();
316 rec.record("a", &[], &Value::Int(1));
317 let h = ReplayHandler::new(&mut rec);
318 assert!(matches!(h.call("x", &[]), ExternalResult::Fallback));
319 assert!(matches!(h.call("a", &[]), ExternalResult::Fallback));
320 }
321}