From 715190927eb105f766cae21994beb14fde1e764e Mon Sep 17 00:00:00 2001 From: piotmag769 Date: Wed, 29 Jul 2026 12:40:07 +0200 Subject: [PATCH] Dict support Closes #149 commit-id:17ae31fb --- src/debugger/state/call_stack.rs | 12 +++ src/debugger/state/call_stack/variables.rs | 98 ++++++++++++++++++- .../state/call_stack/variables/type_name.rs | 5 + 3 files changed, 114 insertions(+), 1 deletion(-) diff --git a/src/debugger/state/call_stack.rs b/src/debugger/state/call_stack.rs index 5f3d236..da0f403 100644 --- a/src/debugger/state/call_stack.rs +++ b/src/debugger/state/call_stack.rs @@ -299,6 +299,18 @@ impl CallStack { var.value = format!("NonZero({})", var.value); var } + CairoValue::Nullable(None) => leaf_variable(name, "null".to_string()), + CairoValue::Nullable(Some(v)) => self.cairo_value_to_variable(name, *v), + CairoValue::Dict { value_type, entries } => { + let type_display = format!("Felt252Dict<{value_type}>"); + if entries.is_empty() { + leaf_variable(name, type_display) + } else { + let children = + entries.into_iter().map(|(key, v)| (format!("[{key}]"), v)).collect(); + self.expandable_variable(name, type_display, children) + } + } } } diff --git a/src/debugger/state/call_stack/variables.rs b/src/debugger/state/call_stack/variables.rs index 2e412ac..4166412 100644 --- a/src/debugger/state/call_stack/variables.rs +++ b/src/debugger/state/call_stack/variables.rs @@ -4,6 +4,7 @@ use cairo_lang_sierra::extensions::core::CoreTypeConcrete; use cairo_lang_sierra::extensions::modules::starknet::StarknetTypeConcrete; use cairo_lang_sierra::ids::ConcreteTypeId; use cairo_lang_sierra::program::StatementIdx; +use cairo_vm::Felt252; use cairo_vm::types::relocatable::{MaybeRelocatable, Relocatable}; use cairo_vm::vm::vm_core::VirtualMachine; use indexmap::IndexMap; @@ -23,13 +24,15 @@ use vm_reader::VmReader; #[derive(Clone, Debug, PartialEq)] pub enum CairoValue { Bool(bool), - FeltLike(cairo_vm::Felt252), + FeltLike(Felt252), Struct { type_name: String, fields: Vec<(String, CairoValue)> }, Enum { type_name: String, variant_name: String, variant_value: Box }, Tuple(Vec), Array { element_type: String, elements: Vec }, Snapshot(Box), NonZero(Box), + Nullable(Option>), + Dict { value_type: String, entries: Vec<(Felt252, CairoValue)> }, Other(String), } @@ -175,6 +178,27 @@ fn maybe_relocatables_to_cairo_value( maybe_relocatables_to_cairo_value(values, &inner.ty, reader, ctx) .map(|v| CairoValue::NonZero(Box::new(v))) } + CoreTypeConcrete::Nullable(inner) => match values[0] { + MaybeRelocatable::Int(f) if f == Felt252::ZERO => Some(CairoValue::Nullable(None)), + MaybeRelocatable::RelocatableValue(boxed_ptr) => { + let size = ctx.type_size(&inner.ty); + let boxed_values = (0..size) + .map(|i| { + reader.read_relocatable(Relocatable { + segment_index: boxed_ptr.segment_index, + offset: boxed_ptr.offset + i, + }) + }) + .collect::>>()?; + let value = + maybe_relocatables_to_cairo_value(&boxed_values, &inner.ty, reader, ctx)?; + Some(CairoValue::Nullable(Some(Box::new(value)))) + } + MaybeRelocatable::Int(_) => { + warn!("expected zero or relocatable for nullable, got nonzero felt"); + None + } + }, CoreTypeConcrete::Struct(struct_type) => { let type_long_id = &ctx.var_type_info(type_id).long_id; let slices = member_slices(&struct_type.members, values, ctx)?; @@ -257,6 +281,33 @@ fn maybe_relocatables_to_cairo_value( variant_value: Box::new(variant_value), }) } + CoreTypeConcrete::Felt252Dict(info) => { + let MaybeRelocatable::RelocatableValue(current) = values[0] else { + warn!("expected relocatable for dict pointer"); + return None; + }; + let start = Relocatable { segment_index: current.segment_index, offset: 0 }; + extract_dict_entries(start, current.offset, &info.ty, reader, ctx) + } + CoreTypeConcrete::Felt252DictEntry(info) => { + let MaybeRelocatable::RelocatableValue(current) = values[0] else { + warn!("expected relocatable for dict entry pointer"); + return None; + }; + let start = Relocatable { segment_index: current.segment_index, offset: 0 }; + // The in-flight access (key + prev_value already written, new_value not yet + // finalized) isn't a complete triple, so it's excluded from the committed contents. + let committed_end = (current.offset / DICT_ACCESS_SIZE) * DICT_ACCESS_SIZE; + extract_dict_entries(start, committed_end, &info.ty, reader, ctx) + } + CoreTypeConcrete::SquashedFelt252Dict(info) => { + let (start_ptr, end_ptr) = extract_array_pointers(values)?; + if start_ptr.segment_index != end_ptr.segment_index { + warn!("squashed dict start and end pointers in different segments"); + return None; + } + extract_dict_entries(start_ptr, end_ptr.offset, &info.ty, reader, ctx) + } CoreTypeConcrete::Starknet(starknet_type) => { let type_name = match starknet_type { StarknetTypeConcrete::ContractAddress(_) => Some("ContractAddress"), @@ -295,6 +346,51 @@ fn fallback_value(values: &[MaybeRelocatable], type_id: &ConcreteTypeId) -> Opti } } +/// Number of felt cells per dict access: `[key, prev_value, new_value]`. +/// Every `felt252_dict` write (via entry get/finalize) or squash appends one such triple to the +/// dict's own memory segment, so the current contents can be reconstructed by scanning it. +const DICT_ACCESS_SIZE: usize = 3; + +/// Reconstructs the current key-value contents of a `Felt252Dict` (or its squashed form) by +/// scanning its backing segment for `[key, prev_value, new_value]` triples from `start` up to +/// (but excluding) `end_offset`. Later triples for the same key override earlier ones, matching +/// the dict's actual current state. +fn extract_dict_entries( + start: Relocatable, + end_offset: usize, + value_type_id: &ConcreteTypeId, + reader: &VmReader<'_>, + ctx: &Context, +) -> Option { + let mut raw_values: IndexMap = IndexMap::new(); + let Relocatable { segment_index, mut offset } = start; + + while offset < end_offset { + let key_cell = Relocatable { segment_index, offset }; + let value_cell = Relocatable { segment_index, offset: offset + 2 }; + + let Some(MaybeRelocatable::Int(key)) = reader.read_relocatable(key_cell) else { + warn!("expected felt for dict key, got relocatable or missing value"); + return None; + }; + let value = reader.read_relocatable(value_cell)?; + + raw_values.insert(key, value); + offset += DICT_ACCESS_SIZE; + } + + let value_type = format_type_name(value_type_id, ctx); + let entries = raw_values + .into_iter() + .map(|(key, value)| { + let value = maybe_relocatables_to_cairo_value(&[value], value_type_id, reader, ctx)?; + Some((key, value)) + }) + .collect::>>()?; + + Some(CairoValue::Dict { value_type, entries }) +} + fn extract_array_pointers(values: &[MaybeRelocatable]) -> Option<(Relocatable, Relocatable)> { if values.len() != 2 { warn!("expected 2 values for array/span, got {}", values.len()); diff --git a/src/debugger/state/call_stack/variables/type_name.rs b/src/debugger/state/call_stack/variables/type_name.rs index e18ae25..dd3ac23 100644 --- a/src/debugger/state/call_stack/variables/type_name.rs +++ b/src/debugger/state/call_stack/variables/type_name.rs @@ -28,6 +28,11 @@ pub fn format_type_name(type_id: &ConcreteTypeId, ctx: &Context) -> String { CoreTypeConcrete::Nullable(inner) => { format!("Nullable<{}>", format_type_name(&inner.ty, ctx)) } + CoreTypeConcrete::Felt252Dict(info) + | CoreTypeConcrete::Felt252DictEntry(info) + | CoreTypeConcrete::SquashedFelt252Dict(info) => { + format!("Felt252Dict<{}>", format_type_name(&info.ty, ctx)) + } CoreTypeConcrete::Felt252(_) => "felt252".to_string(), CoreTypeConcrete::Uint8(_) => "u8".to_string(), CoreTypeConcrete::Uint16(_) => "u16".to_string(),