Skip to main content

miden_debug_engine/debug/
variables.rs

1use alloc::{
2    collections::BTreeMap,
3    rc::Rc,
4    string::{String, ToString},
5    sync::Arc,
6    vec::Vec,
7};
8use core::cell::RefCell;
9
10use miden_assembly_syntax::ast::{
11    DebugFrameBase, DebugLocationExpression, DebugLocationExpressionOp, DebugVarInfo,
12    DebugVarLocation, types::Type,
13};
14use miden_core::Felt;
15use miden_processor::{ContextId, FastProcessor, trace::RowIndex};
16
17use super::InvocationId;
18
19type DebugVarEvents = Rc<RefCell<BTreeMap<RowIndex, Vec<DebugVarInfo>>>>;
20type CapturedDebugValues = Rc<RefCell<BTreeMap<(RowIndex, Arc<str>), Vec<Felt>>>>;
21
22/// A snapshot of a debug variable at a specific clock cycle.
23#[derive(Debug, Clone)]
24pub struct DebugVarSnapshot {
25    /// The clock cycle when this variable info was recorded.
26    pub clk: RowIndex,
27    /// The debug variable information.
28    pub info: DebugVarInfo,
29}
30
31/// A variable resolved in its owning invocation's retained execution context.
32#[derive(Debug, Clone)]
33pub struct ResolvedDebugVariable {
34    pub raw: Option<Felt>,
35    pub value: String,
36    pub type_name: String,
37}
38
39#[derive(Default)]
40struct InvocationVariables {
41    variables: BTreeMap<String, DebugVarSnapshot>,
42    captured: BTreeMap<String, Vec<Felt>>,
43    /// Locations with frame-base anchors refreshed while active and retained while suspended.
44    locations: BTreeMap<String, DebugVarLocation>,
45    context: ContextId,
46    fmp: Option<u32>,
47}
48
49impl InvocationVariables {
50    fn refresh_storage(&mut self, processor: &FastProcessor) {
51        let read_memory = |address: u32| {
52            processor.memory().read_element(self.context, Felt::from_u32(address)).ok()
53        };
54        self.fmp = read_memory(miden_core::FMP_ADDR.as_canonical_u64() as u32)
55            .and_then(|value| u32::try_from(value.as_canonical_u64()).ok());
56        let read_local =
57            |offset: i16| read_memory(self.fmp?.checked_add_signed(i32::from(offset))?);
58        self.locations = self
59            .variables
60            .iter()
61            .map(|(name, variable)| {
62                (
63                    name.clone(),
64                    anchor_frame_bases(variable.info.value_location(), &read_memory, &read_local)
65                        .unwrap_or(DebugVarLocation::Unavailable),
66                )
67            })
68            .collect();
69    }
70}
71
72/// Tracks debug variable snapshots, mapping variable names to their most recent location info.
73pub struct DebugVarTracker {
74    invocations: BTreeMap<InvocationId, InvocationVariables>,
75    current_invocation: Option<InvocationId>,
76    invocation_mode: bool,
77    /// All debug variable events recorded during execution, keyed by clock cycle.
78    events: DebugVarEvents,
79    /// Canonical values frozen at transient debug decorators.
80    captured_values: CapturedDebugValues,
81    /// Current view of variables - maps variable name to most recent info.
82    current_vars: BTreeMap<String, DebugVarSnapshot>,
83    /// Canonical values captured for currently visible transient variables.
84    current_captured_values: BTreeMap<String, Vec<Felt>>,
85    /// The clock cycle up to which we've processed events.
86    processed_up_to: RowIndex,
87}
88
89impl DebugVarTracker {
90    /// Create a new tracker using the given shared event store.
91    pub fn new(events: DebugVarEvents) -> Self {
92        Self {
93            invocations: BTreeMap::new(),
94            current_invocation: None,
95            invocation_mode: false,
96            events,
97            captured_values: Rc::new(Default::default()),
98            current_vars: BTreeMap::new(),
99            current_captured_values: BTreeMap::new(),
100            processed_up_to: RowIndex::from(0),
101        }
102    }
103
104    pub fn current_invocation(&self) -> Option<InvocationId> {
105        self.current_invocation
106    }
107
108    pub fn has_invocation(&self, invocation: InvocationId) -> bool {
109        self.invocations.contains_key(&invocation)
110    }
111
112    pub fn variables_for(
113        &self,
114        invocation: InvocationId,
115    ) -> impl Iterator<Item = &DebugVarSnapshot> {
116        self.invocations
117            .get(&invocation)
118            .into_iter()
119            .flat_map(|frame| frame.variables.values())
120    }
121
122    /// Observe the operation's owner and declarations before executing it. Only observed active
123    /// invocations acquire variable state; recovered ancestors do not inherit a callee's locals.
124    pub fn observe_execution(
125        &mut self,
126        live_ids: impl IntoIterator<Item = InvocationId>,
127        current: Option<InvocationId>,
128        processor: &FastProcessor,
129        clk: RowIndex,
130        mut infos: Vec<DebugVarInfo>,
131    ) {
132        // An external CALL can switch memory contexts before its source forest is entered.
133        // The retained caller identity still appears during that transition, but it must not
134        // acquire the callee's memory context or declarations. An invocation's context is fixed.
135        let current = current.filter(|id| {
136            self.invocations
137                .get(id)
138                .is_none_or(|frame| frame.context == processor.state().ctx())
139        });
140        // The previous operation may have changed a frame base immediately before an exec
141        // boundary. Refresh the departing owner before capturing the new owner's declarations.
142        // CALL/return context switches already suspend the old context without modifying it.
143        if self.current_invocation != current
144            && let Some(previous) = self.current_invocation
145            && let Some(frame) = self.invocations.get_mut(&previous)
146            && frame.context == processor.state().ctx()
147        {
148            frame.refresh_storage(processor);
149        }
150        self.invocation_mode = true;
151        self.current_invocation = current;
152        let live_ids = live_ids.into_iter().collect::<alloc::collections::BTreeSet<_>>();
153        self.invocations.retain(|id, _| live_ids.contains(id));
154        let Some(current) = current else { return };
155        let state = processor.state();
156        let context = state.ctx();
157        let stack = state.get_stack_state();
158        let frame = self.invocations.entry(current).or_default();
159        frame.context = context;
160        let kills = infos.iter().map(is_debug_var_kill).collect::<Vec<_>>();
161        let captured = snapshot_transient_debug_values(&mut infos, &stack);
162        for (info, is_kill) in infos.into_iter().zip(kills) {
163            let name = info.name().to_string();
164            if is_kill {
165                frame.variables.remove(&name);
166                frame.captured.remove(&name);
167                frame.locations.remove(&name);
168            } else {
169                match captured.get(info.name()) {
170                    Some(values) => {
171                        frame.captured.insert(name.clone(), values.clone());
172                    }
173                    None => {
174                        frame.captured.remove(&name);
175                    }
176                }
177                frame.variables.insert(name, DebugVarSnapshot { clk, info });
178            }
179        }
180        frame.refresh_storage(processor);
181    }
182
183    /// Read live storage using the selected invocation's context and retained frame bases.
184    pub fn resolve(
185        &self,
186        invocation: InvocationId,
187        name: &str,
188        processor: &FastProcessor,
189    ) -> Option<ResolvedDebugVariable> {
190        let frame = self.invocations.get(&invocation)?;
191        let variable = frame.variables.get(name)?;
192        let read_memory = |address: u32| {
193            processor.memory().read_element(frame.context, Felt::from_u32(address)).ok()
194        };
195        let active =
196            self.current_invocation == Some(invocation) && processor.state().ctx() == frame.context;
197        let fmp = if active {
198            read_memory(miden_core::FMP_ADDR.as_canonical_u64() as u32)
199                .and_then(|value| u32::try_from(value.as_canonical_u64()).ok())
200        } else {
201            frame.fmp
202        };
203        let read_local = |offset: i16| read_memory(fmp?.checked_add_signed(i32::from(offset))?);
204        let location = if active {
205            anchor_frame_bases(variable.info.value_location(), &read_memory, &read_local)
206                .unwrap_or(DebugVarLocation::Unavailable)
207        } else {
208            frame.locations.get(name)?.clone()
209        };
210        let captured = frame.captured.get(name);
211        let raw = captured
212            .and_then(|values| values.first().copied())
213            .or_else(|| resolve_variable_value(&location, &[], read_memory, read_local));
214        let type_name =
215            variable.info.ty().map(ToString::to_string).unwrap_or_else(|| "Felt".into());
216        let typed = variable.info.ty().and_then(|ty| {
217            super::format_value(ty, |count| {
218                captured.filter(|values| values.len() == count).cloned().or_else(|| {
219                    resolve_typed_variable_values(
220                        &location,
221                        ty,
222                        count,
223                        &[],
224                        read_memory,
225                        read_local,
226                    )
227                })
228            })
229        });
230        let value = typed
231            .or_else(|| raw.map(|value| value.as_canonical_u64().to_string()))
232            .unwrap_or_else(|| "<unavailable>".into());
233        Some(ResolvedDebugVariable {
234            raw,
235            value,
236            type_name,
237        })
238    }
239
240    /// Record debug variable events at the given clock cycle.
241    pub fn record_events(&self, clk: RowIndex, infos: Vec<DebugVarInfo>) {
242        if !infos.is_empty() {
243            self.events.borrow_mut().entry(clk).or_default().extend(infos);
244        }
245    }
246
247    /// Record debug variable events after freezing transient stack-backed values.
248    pub fn record_events_with_stack(
249        &self,
250        clk: RowIndex,
251        mut infos: Vec<DebugVarInfo>,
252        stack: &[Felt],
253    ) {
254        let names = infos.iter().map(|info| info.name().clone()).collect::<Vec<_>>();
255        let captured_values = snapshot_transient_debug_values(&mut infos, stack);
256        {
257            let mut stored_values = self.captured_values.borrow_mut();
258            for name in names {
259                stored_values.remove(&(clk, name));
260            }
261            stored_values
262                .extend(captured_values.into_iter().map(|(name, values)| ((clk, name), values)));
263        }
264        self.record_events(clk, infos);
265    }
266
267    /// Process all events up to and including `clk`, updating current variable state.
268    pub fn update_to_cycle(&mut self, clk: RowIndex) {
269        let events = self.events.borrow();
270        let captured_values = self.captured_values.borrow();
271
272        // Process events from processed_up_to to clk
273        for (event_clk, var_infos) in events.range(self.processed_up_to..=clk) {
274            for info in var_infos {
275                if is_debug_var_kill(info) {
276                    self.current_vars.remove(info.name().as_ref());
277                    self.current_captured_values.remove(info.name().as_ref());
278                    continue;
279                }
280                let snapshot = DebugVarSnapshot {
281                    clk: *event_clk,
282                    info: info.clone(),
283                };
284                let name = info.name().to_string();
285                match captured_values.get(&(*event_clk, info.name().clone())) {
286                    Some(values) => {
287                        self.current_captured_values.insert(name.clone(), values.clone());
288                    }
289                    None => {
290                        self.current_captured_values.remove(&name);
291                    }
292                }
293                self.current_vars.insert(name, snapshot);
294            }
295        }
296
297        self.processed_up_to = clk;
298    }
299
300    /// Reset the tracker to the beginning of execution.
301    pub fn reset(&mut self) {
302        self.invocations.clear();
303        self.current_invocation = None;
304        self.current_vars.clear();
305        self.current_captured_values.clear();
306        self.processed_up_to = RowIndex::from(0);
307    }
308
309    fn active_variables(&self) -> Option<&BTreeMap<String, DebugVarSnapshot>> {
310        if self.invocation_mode {
311            Some(&self.invocations.get(&self.current_invocation?)?.variables)
312        } else {
313            Some(&self.current_vars)
314        }
315    }
316
317    /// Get all currently visible variables.
318    pub fn current_variables(&self) -> impl Iterator<Item = &DebugVarSnapshot> {
319        self.active_variables().into_iter().flat_map(|variables| variables.values())
320    }
321
322    /// Get a specific variable by name.
323    pub fn get_variable(&self, name: &str) -> Option<&DebugVarSnapshot> {
324        self.active_variables()?.get(name)
325    }
326
327    /// Returns canonical values captured at the variable's latest debug decorator.
328    pub fn captured_values(&self, name: &str) -> Option<&[Felt]> {
329        if self.invocation_mode {
330            self.invocations
331                .get(&self.current_invocation?)?
332                .captured
333                .get(name)
334                .map(Vec::as_slice)
335        } else {
336            self.current_captured_values.get(name).map(Vec::as_slice)
337        }
338    }
339
340    /// Get the number of tracked variables.
341    pub fn variable_count(&self) -> usize {
342        self.active_variables().map_or(0, BTreeMap::len)
343    }
344
345    /// Check if there are any tracked variables.
346    pub fn has_variables(&self) -> bool {
347        self.variable_count() != 0
348    }
349}
350
351/// Only frame-base operands are anchored. Ordinary memory reads remain live, including pointers
352/// read by general location expressions. The original declaration is retained for the next refresh.
353fn anchor_frame_bases(
354    location: &DebugVarLocation,
355    read_memory: &impl Fn(u32) -> Option<Felt>,
356    read_local: &impl Fn(i16) -> Option<Felt>,
357) -> Option<DebugVarLocation> {
358    let operations = match location {
359        DebugVarLocation::ResolvedFrameBase { base, byte_offset } => vec![
360            DebugLocationExpressionOp::ConstU64(resolve_frame_base_address(
361                *base,
362                *byte_offset,
363                read_memory,
364                read_local,
365            )?),
366            DebugLocationExpressionOp::DerefBytes,
367        ],
368        DebugVarLocation::Expression(expression) => expression
369            .operations()
370            .iter()
371            .map(|operation| match operation {
372                DebugLocationExpressionOp::FrameBaseAddress { base, byte_offset } => {
373                    Some(DebugLocationExpressionOp::ConstU64(resolve_frame_base_address(
374                        *base,
375                        *byte_offset,
376                        read_memory,
377                        read_local,
378                    )?))
379                }
380                operation => Some(*operation),
381            })
382            .collect::<Option<Vec<_>>>()?,
383        location => return Some(location.clone()),
384    };
385    Some(DebugVarLocation::Expression(DebugLocationExpression::new(operations).ok()?))
386}
387
388/// Snapshot transient debug locations at the decorator point.
389///
390/// Stack locations are only meaningful at the debug decorator itself. Keeping them live and
391/// resolving them against a later VM stack can report unrelated values. Memory, local, and
392/// frame-base declarations describe live storage and must be resolved against the current VM state
393/// when the user inspects variables.
394pub fn snapshot_transient_debug_values(
395    infos: &mut [DebugVarInfo],
396    stack: &[Felt],
397) -> BTreeMap<Arc<str>, Vec<Felt>> {
398    let mut captured_values = BTreeMap::new();
399
400    for info in infos {
401        captured_values.remove(info.name().as_ref());
402        match info.value_location() {
403            DebugVarLocation::Stack(position) => {
404                let count = info.ty().and_then(super::abi_types::value_felt_count).unwrap_or(1);
405                let start = *position as usize;
406                let values = start
407                    .checked_add(count)
408                    .and_then(|end| stack.get(start..end))
409                    .map(<[Felt]>::to_vec);
410                let location = values
411                    .as_ref()
412                    .and_then(|values| values.first().copied())
413                    .or_else(|| (count == 0).then(|| Felt::from_u32(0)))
414                    .map(DebugVarLocation::Const)
415                    .unwrap_or(DebugVarLocation::Unavailable);
416                if let Some(values) = values {
417                    captured_values.insert(info.name().clone(), values);
418                }
419                info.set_value_location(location);
420            }
421            DebugVarLocation::Expression(expression) => {
422                let operations = expression
423                    .operations()
424                    .iter()
425                    .map(|operation| match operation {
426                        DebugLocationExpressionOp::ReadStack(position) => stack
427                            .get(*position as usize)
428                            .map(|value| {
429                                DebugLocationExpressionOp::ConstU64(value.as_canonical_u64())
430                            })
431                            .ok_or(()),
432                        operation => Ok(*operation),
433                    })
434                    .collect::<Result<Vec<_>, _>>();
435                let location = operations
436                    .and_then(|ops| DebugLocationExpression::new(ops).map_err(|_| ()))
437                    .map(DebugVarLocation::Expression)
438                    .unwrap_or(DebugVarLocation::Unavailable);
439                info.set_value_location(location);
440            }
441            _ => {}
442        }
443    }
444
445    captured_values
446}
447
448fn is_debug_var_kill(info: &DebugVarInfo) -> bool {
449    matches!(info.value_location(), DebugVarLocation::Unavailable)
450}
451
452/// Resolve a debug variable's value given its location and the current VM state.
453pub fn resolve_variable_value(
454    location: &DebugVarLocation,
455    stack: &[Felt],
456    get_memory: impl Fn(u32) -> Option<Felt>,
457    get_local: impl Fn(i16) -> Option<Felt>,
458) -> Option<Felt> {
459    resolve_variable_values(location, 1, stack, get_memory, get_local)?.pop()
460}
461
462/// Resolve one or more consecutive felts for a debug variable.
463pub fn resolve_variable_values(
464    location: &DebugVarLocation,
465    count: usize,
466    stack: &[Felt],
467    get_memory: impl Fn(u32) -> Option<Felt>,
468    get_local: impl Fn(i16) -> Option<Felt>,
469) -> Option<Vec<Felt>> {
470    if count == 0 {
471        return Some(Vec::new());
472    }
473
474    match location {
475        DebugVarLocation::Stack(pos) => {
476            let start = *pos as usize;
477            let end = start.checked_add(count)?;
478            Some(stack.get(start..end)?.to_vec())
479        }
480        DebugVarLocation::Memory(addr) => resolve_consecutive_memory(*addr, count, &get_memory),
481        DebugVarLocation::Const(felt) => (count == 1).then_some(vec![*felt]),
482        DebugVarLocation::Local(offset) => {
483            let mut values = Vec::with_capacity(count);
484            for index in 0..count {
485                let index = i16::try_from(index).ok()?;
486                values.push(get_local(offset.checked_add(index)?)?);
487            }
488            Some(values)
489        }
490        DebugVarLocation::ResolvedFrameBase { base, byte_offset } => {
491            resolve_frame_base_values(*base, *byte_offset, count, &get_memory, &get_local)
492        }
493        DebugVarLocation::Expression(expression) => {
494            match resolve_expression(expression.operations(), stack, &get_memory, &get_local)? {
495                ResolvedExpression::Scalar(value) => {
496                    (count == 1).then(|| integer_to_felt(value)).flatten().map(|value| vec![value])
497                }
498                ResolvedExpression::ByteAddress(address) => {
499                    let element_address = u32::try_from(address / 4).ok()?;
500                    resolve_consecutive_memory(element_address, count, &get_memory)
501                }
502            }
503        }
504        DebugVarLocation::Unavailable => None,
505    }
506}
507
508/// Resolve a typed debug variable into the canonical ABI felts consumed by the typed decoder.
509///
510/// Simple Miden locations already contain canonical stack values. Frame-base locations and a
511/// terminal [`DebugLocationExpressionOp::DerefBytes`] instead identify packed Rust memory, which
512/// must be read at byte granularity and lifted according to `ty` before it can be decoded.
513pub fn resolve_typed_variable_values(
514    location: &DebugVarLocation,
515    ty: &Type,
516    count: usize,
517    stack: &[Felt],
518    get_memory: impl Fn(u32) -> Option<Felt>,
519    get_local: impl Fn(i16) -> Option<Felt>,
520) -> Option<Vec<Felt>> {
521    match location {
522        DebugVarLocation::ResolvedFrameBase { base, byte_offset } => {
523            let byte_address =
524                resolve_frame_base_address(*base, *byte_offset, &get_memory, &get_local)?;
525            resolve_typed_memory_value(ty, byte_address, count, &get_memory)
526        }
527        DebugVarLocation::Expression(expression) => {
528            match resolve_expression(expression.operations(), stack, &get_memory, &get_local)? {
529                ResolvedExpression::ByteAddress(byte_address) => {
530                    resolve_typed_memory_value(ty, byte_address, count, &get_memory)
531                }
532                ResolvedExpression::Scalar(value) => {
533                    (count == 1).then(|| integer_to_felt(value)).flatten().map(|value| vec![value])
534                }
535            }
536        }
537        _ => resolve_variable_values(location, count, stack, get_memory, get_local),
538    }
539}
540
541fn resolve_consecutive_memory(
542    start_addr: u32,
543    count: usize,
544    get_memory: &impl Fn(u32) -> Option<Felt>,
545) -> Option<Vec<Felt>> {
546    let mut values = Vec::with_capacity(count);
547    for index in 0..count {
548        let addr = start_addr.checked_add(u32::try_from(index).ok()?)?;
549        values.push(get_memory(addr)?);
550    }
551    Some(values)
552}
553
554#[derive(Clone, Copy, Debug, Eq, PartialEq)]
555enum ResolvedExpression {
556    Scalar(i128),
557    ByteAddress(u64),
558}
559
560fn resolve_expression(
561    ops: &[DebugLocationExpressionOp],
562    stack: &[Felt],
563    get_memory: &impl Fn(u32) -> Option<Felt>,
564    get_local: &impl Fn(i16) -> Option<Felt>,
565) -> Option<ResolvedExpression> {
566    let mut values = Vec::<i128>::new();
567
568    for (index, op) in ops.iter().enumerate() {
569        match op {
570            DebugLocationExpressionOp::ReadStack(index) => {
571                values.push(i128::from(stack.get(*index as usize)?.as_canonical_u64()));
572            }
573            DebugLocationExpressionOp::ReadMemory(index) => {
574                values.push(i128::from(get_memory(*index)?.as_canonical_u64()));
575            }
576            DebugLocationExpressionOp::ReadLocal(index) => {
577                values.push(i128::from(get_local(*index)?.as_canonical_u64()));
578            }
579            DebugLocationExpressionOp::ConstU64(value) => {
580                values.push(i128::from(*value));
581            }
582            DebugLocationExpressionOp::ConstI64(value) => {
583                values.push(i128::from(*value));
584            }
585            DebugLocationExpressionOp::AddUnsigned(value) => {
586                let lhs = values.pop()?;
587                values.push(lhs.checked_add(i128::from(*value))?);
588            }
589            DebugLocationExpressionOp::Add => {
590                let rhs = values.pop()?;
591                let lhs = values.pop()?;
592                values.push(lhs.checked_add(rhs)?);
593            }
594            DebugLocationExpressionOp::Sub => {
595                let rhs = values.pop()?;
596                let lhs = values.pop()?;
597                values.push(lhs.checked_sub(rhs)?);
598            }
599            DebugLocationExpressionOp::DerefBytes => {
600                let byte_address = u64::try_from(values.pop()?).ok()?;
601                if index + 1 == ops.len() {
602                    return Some(ResolvedExpression::ByteAddress(byte_address));
603                }
604                let element_address = u32::try_from(byte_address / 4).ok()?;
605                values.push(i128::from(get_memory(element_address)?.as_canonical_u64()));
606            }
607            DebugLocationExpressionOp::FrameBaseAddress { base, byte_offset } => {
608                values.push(i128::from(resolve_frame_base_address(
609                    *base,
610                    *byte_offset,
611                    get_memory,
612                    get_local,
613                )?));
614            }
615        }
616    }
617
618    values.pop().map(ResolvedExpression::Scalar)
619}
620
621fn resolve_frame_base_values(
622    base: DebugFrameBase,
623    byte_offset: i64,
624    count: usize,
625    get_memory: &impl Fn(u32) -> Option<Felt>,
626    get_local: &impl Fn(i16) -> Option<Felt>,
627) -> Option<Vec<Felt>> {
628    let byte_address = resolve_frame_base_address(base, byte_offset, get_memory, get_local)?;
629    resolve_byte_address_values(byte_address, count, get_memory)
630}
631
632fn resolve_frame_base_address(
633    base: DebugFrameBase,
634    byte_offset: i64,
635    get_memory: &impl Fn(u32) -> Option<Felt>,
636    get_local: &impl Fn(i16) -> Option<Felt>,
637) -> Option<u64> {
638    let base = match base {
639        DebugFrameBase::Local(offset) => get_local(offset)?,
640        DebugFrameBase::Memory(address) => get_memory(address)?,
641    };
642    base.as_canonical_u64().checked_add_signed(byte_offset)
643}
644
645fn resolve_byte_address_values(
646    byte_address: u64,
647    count: usize,
648    get_memory: &impl Fn(u32) -> Option<Felt>,
649) -> Option<Vec<Felt>> {
650    if !byte_address.is_multiple_of(4) {
651        return None;
652    }
653    let element_address = u32::try_from(byte_address / 4).ok()?;
654    resolve_consecutive_memory(element_address, count, get_memory)
655}
656
657fn integer_to_felt(value: i128) -> Option<Felt> {
658    Felt::new(u64::try_from(value).ok()?).ok()
659}
660
661fn read_memory_bytes(
662    byte_address: u64,
663    size: usize,
664    get_memory: &impl Fn(u32) -> Option<Felt>,
665) -> Option<Vec<u8>> {
666    if size == 0 {
667        return Some(Vec::new());
668    }
669
670    let element_address = u32::try_from(byte_address / 4).ok()?;
671    let byte_offset = usize::try_from(byte_address % 4).ok()?;
672    let end = byte_offset.checked_add(size)?;
673    let element_count = end.div_ceil(4);
674    let mut bytes = Vec::with_capacity(element_count.checked_mul(4)?);
675
676    for index in 0..element_count {
677        let address = element_address.checked_add(u32::try_from(index).ok()?)?;
678        let value = get_memory(address)?.as_canonical_u64() as u32;
679        bytes.extend_from_slice(&value.to_le_bytes());
680    }
681
682    Some(bytes.get(byte_offset..end)?.to_vec())
683}
684
685fn resolve_typed_memory_value(
686    ty: &Type,
687    byte_address: u64,
688    expected_count: usize,
689    get_memory: &impl Fn(u32) -> Option<Felt>,
690) -> Option<Vec<Felt>> {
691    let mut values = Vec::with_capacity(expected_count);
692    append_typed_memory_value(ty, byte_address, get_memory, &mut values)?;
693    (values.len() == expected_count).then_some(values)
694}
695
696fn append_typed_memory_value(
697    ty: &Type,
698    byte_address: u64,
699    get_memory: &impl Fn(u32) -> Option<Felt>,
700    values: &mut Vec<Felt>,
701) -> Option<()> {
702    match ty {
703        Type::Felt => {
704            if !byte_address.is_multiple_of(4) {
705                return None;
706            }
707            values.push(get_memory(u32::try_from(byte_address / 4).ok()?)?);
708        }
709        Type::I1 => {
710            let byte = *read_memory_bytes(byte_address, 1, get_memory)?.first()?;
711            values.push(Felt::from_u32(u32::from(byte & 1)));
712        }
713        Type::I8 | Type::I16 | Type::I32 | Type::I64 | Type::I128 => {
714            let value = read_unsigned_integer(ty, byte_address, get_memory)?;
715            let bit_width = u32::try_from(ty.size_in_bits()).ok()?;
716            let shift = 128_u32.checked_sub(bit_width)?;
717            let signed = ((value << shift) as i128) >> shift;
718            let slot_bits = u32::try_from(ty.size_in_felts().checked_mul(32)?).ok()?;
719            let canonical = (signed as u128) & low_bits_mask(slot_bits);
720            append_u128_limbs(canonical, ty.size_in_felts(), values);
721        }
722        Type::U8 | Type::U16 | Type::U32 | Type::U64 | Type::U128 | Type::F64 => {
723            let value = read_unsigned_integer(ty, byte_address, get_memory)?;
724            append_u128_limbs(value, ty.size_in_felts(), values);
725        }
726        Type::Struct(struct_ty) => {
727            for field in struct_ty.get().fields() {
728                let field_address = byte_address.checked_add(u64::from(field.offset))?;
729                append_typed_memory_value(&field.ty, field_address, get_memory, values)?;
730            }
731        }
732        Type::Array(array_ty) => {
733            let element_size = array_ty.ty.size_in_bytes();
734            let alignment = array_ty.ty.min_alignment();
735            let stride = align_up(element_size, alignment)?;
736            for index in 0..array_ty.len {
737                let offset = index.checked_mul(stride)?;
738                let element_address = byte_address.checked_add(u64::try_from(offset).ok()?)?;
739                append_typed_memory_value(&array_ty.ty, element_address, get_memory, values)?;
740            }
741        }
742        Type::U256
743        | Type::List(_)
744        | Type::Ptr(_)
745        | Type::Function(_)
746        | Type::Enum(_)
747        | Type::Unknown
748        | Type::Never
749        | Type::Variadic => return None,
750    }
751
752    Some(())
753}
754
755fn read_unsigned_integer(
756    ty: &Type,
757    byte_address: u64,
758    get_memory: &impl Fn(u32) -> Option<Felt>,
759) -> Option<u128> {
760    let bytes = read_memory_bytes(byte_address, ty.size_in_bytes(), get_memory)?;
761    let mut value = 0_u128;
762    for (index, byte) in bytes.into_iter().enumerate() {
763        value |= u128::from(byte) << index.checked_mul(8)?;
764    }
765    Some(value)
766}
767
768fn append_u128_limbs(value: u128, count: usize, values: &mut Vec<Felt>) {
769    values.extend((0..count).map(|index| Felt::from_u32((value >> (index * 32)) as u32)));
770}
771
772fn low_bits_mask(bits: u32) -> u128 {
773    if bits == 128 {
774        u128::MAX
775    } else {
776        (1_u128 << bits) - 1
777    }
778}
779
780fn align_up(size: usize, alignment: usize) -> Option<usize> {
781    if size == 0 {
782        return Some(0);
783    }
784    size.checked_add(alignment.checked_sub(1)?)?
785        .checked_div(alignment)?
786        .checked_mul(alignment)
787}
788
789#[cfg(test)]
790mod tests;