From 03fbb9998f8dd79b4de2ce390293ce6a63d2b9af Mon Sep 17 00:00:00 2001 From: piotmag769 Date: Mon, 1 Jun 2026 16:23:14 +0200 Subject: [PATCH] Add arrays and span support commit-id:fb2620cb --- src/debugger/state/call_stack.rs | 24 ++ src/debugger/state/call_stack/variables.rs | 351 +++++++++++++-------- 2 files changed, 247 insertions(+), 128 deletions(-) diff --git a/src/debugger/state/call_stack.rs b/src/debugger/state/call_stack.rs index 281cadb..a3cc44d 100644 --- a/src/debugger/state/call_stack.rs +++ b/src/debugger/state/call_stack.rs @@ -284,6 +284,30 @@ impl CallStack { ..Default::default() } } + CairoValue::Array { elements } => { + if elements.is_empty() { + Variable { + name, + value: "[]".to_string(), + variables_reference: 0, + ..Default::default() + } + } else { + let len = elements.len(); + let children = elements + .into_iter() + .enumerate() + .map(|(i, v)| (format!("[{i}]"), v)) + .collect(); + let ref_id = self.register_children(children); + Variable { + name, + value: format!("[{len}]"), + variables_reference: ref_id, + ..Default::default() + } + } + } CairoValue::Other(value) => { Variable { name, value, variables_reference: 0, ..Default::default() } } diff --git a/src/debugger/state/call_stack/variables.rs b/src/debugger/state/call_stack/variables.rs index f62ed25..760329f 100644 --- a/src/debugger/state/call_stack/variables.rs +++ b/src/debugger/state/call_stack/variables.rs @@ -6,6 +6,7 @@ use cairo_lang_sierra::extensions::modules::starknet::StarknetTypeConcrete; use cairo_lang_sierra::ids::ConcreteTypeId; use cairo_lang_sierra::program::{ConcreteTypeLongId, GenericArg}; use cairo_vm::Felt252; +use cairo_vm::types::relocatable::{MaybeRelocatable, Relocatable}; use cairo_vm::vm::vm_core::VirtualMachine; use indexmap::IndexMap; use starknet_types_core::felt::{Felt, NonZeroFelt}; @@ -23,6 +24,7 @@ pub enum CairoValue { Struct { type_name: String, fields: Vec<(String, CairoValue)> }, Enum { type_name: String, variant_name: String, variant_value: Box }, Tuple(Vec), + Array { elements: Vec }, Snapshot(Box), NonZero(Box), Other(String), @@ -33,7 +35,7 @@ pub fn get_values_of_variables( vm: &VirtualMachine, post_statements_registers: &PostStatementsRegisters, ) -> FunctionVariables { - let mut current_var_values: IndexMap)> = + let mut current_var_values: IndexMap = IndexMap::new(); for (idx, (branch_target, registers_values)) in post_statements_registers { @@ -60,19 +62,20 @@ pub fn get_values_of_variables( continue; } - let cells_vals: Vec<_> = ref_expr - .cells - .iter() - .filter_map(|cell| maybe_extract_felt_from_cell(cell, registers_values, vm)) - .collect(); + let num_cells = ref_expr.cells.len(); - // Skip if there was an error while extracting felts since it makes us unable to rely on - // type sizes (which we have to rely on). - if cells_vals.len() != ref_expr.cells.len() { + // TODO(#128): fix unit type mappings + if num_cells == 0 { continue; } - if let Some((curr_span, _, curr_cells)) = current_var_values.get(name) { + let Some(value) = + extract_var_value(&ref_expr.cells, &type_id, registers_values, vm, ctx) + else { + continue; + }; + + if let Some((curr_span, curr_num_cells, _)) = current_var_values.get(name) { // If there is a var with the same name in the map already, // and it is further in the code, ignore the current var. if span.start.line < curr_span.start.line @@ -85,84 +88,122 @@ pub fn get_values_of_variables( // The same definition span but fewer cells: a struct_deconstruct intermediate field // variable can be mapped back to the struct's Cairo name in debugger mappings, // which would degrade a complete multi-felt struct to just its first field. - if span == curr_span && cells_vals.len() < curr_cells.len() { + if span == curr_span && num_cells < *curr_num_cells { continue; } } - // TODO(#128): fix unit type mappings - if cells_vals.is_empty() { - continue; - } - - current_var_values.insert(name.clone(), (span.clone(), type_id, cells_vals)); + current_var_values.insert(name.clone(), (span.clone(), num_cells, value)); } // TODO(#99): drop consumed values. } - let names_to_values = current_var_values - .into_iter() - .map(|(name, (_, type_id, felts))| { - let value = felts_to_cairo_value(&felts, &type_id, ctx); - (name, value) - }) - .collect(); + let names_to_values = + current_var_values.into_iter().map(|(name, (_, _, value))| (name, value)).collect(); FunctionVariables { names_to_values } } -fn felts_to_cairo_value(felts: &[Felt252], type_id: &ConcreteTypeId, ctx: &Context) -> CairoValue { +fn extract_var_value( + cells: &[CellExpression], + type_id: &ConcreteTypeId, + registers_values: &RegistersValues, + vm: &VirtualMachine, + ctx: &Context, +) -> Option { + let values = cells + .iter() + .map(|cell| maybe_extract_maybe_relocatable_from_cell(cell, registers_values, vm)) + .collect::>>()?; + + maybe_relocatables_to_cairo_value(&values, type_id, vm, ctx) +} + +fn maybe_relocatables_to_cairo_value( + values: &[MaybeRelocatable], + type_id: &ConcreteTypeId, + vm: &VirtualMachine, + ctx: &Context, +) -> Option { let Some(concrete_type) = ctx.get_concrete_type(type_id) else { - return fallback_value(felts, type_id); + return fallback_value(values, type_id); }; match concrete_type { + CoreTypeConcrete::Array(info) | CoreTypeConcrete::Span(info) => { + let (start_ptr, end_ptr) = extract_array_pointers(values)?; + extract_array_from_pointers(start_ptr, end_ptr, &info.ty, vm, ctx) + } + CoreTypeConcrete::Snapshot(inner) => { + maybe_relocatables_to_cairo_value(values, &inner.ty, vm, ctx) + .map(|v| CairoValue::Snapshot(Box::new(v))) + } + CoreTypeConcrete::NonZero(inner) => { + maybe_relocatables_to_cairo_value(values, &inner.ty, vm, ctx) + .map(|v| CairoValue::NonZero(Box::new(v))) + } CoreTypeConcrete::Struct(struct_type) => { let type_long_id = &ctx.var_type_info(type_id).long_id; if is_tuple(type_long_id) { - let fields = - collect_member_fields(&struct_type.members, felts, ctx, |_| String::new()) - .into_iter() - .map(|(_, v)| v) - .collect(); - CairoValue::Tuple(fields) - } else { - let struct_info = ctx.struct_info(type_id); - let type_name = struct_info - .map(|info| info.name.clone()) - .unwrap_or_else(|| extract_short_type_name(type_id)); - let fields = collect_member_fields(&struct_type.members, felts, ctx, |i| { - struct_info - .and_then(|info| info.members.get(i)) - .cloned() - .unwrap_or_else(|| format!(".{i}")) - }); - CairoValue::Struct { type_name, fields } + let mut offset = 0; + let mut elements = Vec::with_capacity(struct_type.members.len()); + for member_type_id in &struct_type.members { + let size = ctx.type_size(member_type_id); + let member_values = &values[offset..offset + size]; + offset += size; + elements.push(maybe_relocatables_to_cairo_value( + member_values, + member_type_id, + vm, + ctx, + )?); + } + return Some(CairoValue::Tuple(elements)); + } + + let struct_info = ctx.struct_info(type_id); + let type_name = struct_info + .map(|info| info.name.clone()) + .unwrap_or_else(|| extract_short_type_name(type_id)); + let mut offset = 0; + let mut fields = Vec::with_capacity(struct_type.members.len()); + for (i, member_type_id) in struct_type.members.iter().enumerate() { + let size = ctx.type_size(member_type_id); + let member_values = &values[offset..offset + size]; + offset += size; + let name = struct_info + .and_then(|info| info.members.get(i)) + .cloned() + .unwrap_or_else(|| format!(".{i}")); + let value = + maybe_relocatables_to_cairo_value(member_values, member_type_id, vm, ctx)?; + fields.push((name, value)); } + Some(CairoValue::Struct { type_name, fields }) } CoreTypeConcrete::Enum(enum_type) => { let type_long_id = &ctx.var_type_info(type_id).long_id; + let Some(MaybeRelocatable::Int(discriminant_felt)) = values.first() else { + warn!("expected felt for enum discriminant, got relocatable or empty slice"); + return None; + }; if is_bool(type_long_id) { - let stored_discriminant: usize = felts[0] + let stored: usize = discriminant_felt .as_ref() .to_biguint() .try_into() .expect("bool discriminant larger than usize::MAX"); - return CairoValue::Bool(stored_discriminant != 0); + return Some(CairoValue::Bool(stored != 0)); } - let enum_info = ctx.enum_info(type_id); - let type_name = enum_info - .map(|info| info.name.clone()) - .unwrap_or_else(|| extract_short_type_name(type_id)); - let n_variants = enum_type.variants.len(); - let stored_discriminant: usize = felts[0] + let stored_discriminant: usize = discriminant_felt .as_ref() .to_biguint() .try_into() .expect("enum discriminant larger than usize::MAX"); + let n_variants = enum_type.variants.len(); // For n <= 2 variants: stored value == variant index directly. // For n > 2 variants: Sierra stores a jump-table offset: stored = 2 * (n - index) - 1, // so index = n - (stored + 1) / 2. @@ -175,21 +216,24 @@ fn felts_to_cairo_value(felts: &[Felt252], type_id: &ConcreteTypeId, ctx: &Conte let variant_id = &enum_type.variants[variant_index]; let variant_size = ctx.type_size(variant_id); // Sierra pads enum payloads at the START: [discriminant | padding | variant_data]. - // The variant's data occupies the last `variant_size` felts of the payload. - let payload = &felts[1..]; - let variant_felts = &payload[payload.len() - variant_size..]; - let variant_value = felts_to_cairo_value(variant_felts, variant_id, ctx); + // The variant's data occupies the last `variant_size` values of the payload. + let payload = &values[1..]; + let variant_values = &payload[payload.len() - variant_size..]; + let variant_value = + maybe_relocatables_to_cairo_value(variant_values, variant_id, vm, ctx)?; + let enum_info = ctx.enum_info(type_id); + let type_name = enum_info + .map(|info| info.name.clone()) + .unwrap_or_else(|| extract_short_type_name(type_id)); let variant_name = enum_info .and_then(|info| info.variants.get(variant_index)) .cloned() .unwrap_or_else(|| format!("variant_{variant_index}")); - CairoValue::Enum { type_name, variant_name, variant_value: Box::new(variant_value) } - } - CoreTypeConcrete::NonZero(inner) => { - CairoValue::NonZero(felts_to_cairo_value(felts, &inner.ty, ctx).into()) - } - CoreTypeConcrete::Snapshot(inner) => { - CairoValue::Snapshot(felts_to_cairo_value(felts, &inner.ty, ctx).into()) + Some(CairoValue::Enum { + type_name, + variant_name, + variant_value: Box::new(variant_value), + }) } CoreTypeConcrete::Starknet(starknet_type) => { let type_name = match starknet_type { @@ -199,22 +243,33 @@ fn felts_to_cairo_value(felts: &[Felt252], type_id: &ConcreteTypeId, ctx: &Conte StarknetTypeConcrete::StorageBaseAddress(_) => Some("StorageBaseAddress"), _ => None, }; + let MaybeRelocatable::Int(felt) = &values[0] else { + unreachable!("starknet type expected to be a felt") + }; match type_name { - Some(name) => CairoValue::Other(format!("{name}(0x{:x})", felts[0].to_biguint())), - None => fallback_value(felts, type_id), + Some(name) => Some(CairoValue::Other(format!("{name}(0x{:x})", felt.to_biguint()))), + None => fallback_value(values, type_id), } } - _ => fallback_value(felts, type_id), + _ => fallback_value(values, type_id), } } -fn fallback_value(felts: &[Felt252], type_id: &ConcreteTypeId) -> CairoValue { - if felts.len() == 1 { - CairoValue::FeltLike(felts[0]) - } else { - warn!("unhandled multi-felt value for type {type_id:?}: {felts:?}"); - let joined = felts.iter().map(|f| f.to_string()).collect::>().join(", "); - CairoValue::Other(format!("[{joined}]")) +fn fallback_value(values: &[MaybeRelocatable], type_id: &ConcreteTypeId) -> Option { + match values { + [MaybeRelocatable::Int(f)] => Some(CairoValue::FeltLike(*f)), + _ => { + let joined = values + .iter() + .filter_map(|v| match v { + MaybeRelocatable::RelocatableValue(_) => None, + MaybeRelocatable::Int(f) => Some(f.to_string()), + }) + .collect::>() + .join(", "); + warn!("unhandled multi-felt value for type {type_id:?}"); + Some(CairoValue::Other(format!("[{joined}]"))) + } } } @@ -227,29 +282,6 @@ fn extract_short_type_name(type_id: &ConcreteTypeId) -> String { format!("type_{}", type_id.id) } -fn collect_member_fields( - members: &[ConcreteTypeId], - felts: &[Felt252], - ctx: &Context, - name_fn: F, -) -> Vec<(String, CairoValue)> -where - F: Fn(usize) -> String, -{ - let mut offset = 0; - members - .iter() - .enumerate() - .map(|(i, concrete_type_id)| { - let size = ctx.type_size(concrete_type_id); - let member_felts = &felts[offset..offset + size]; - offset += size; - let value = felts_to_cairo_value(member_felts, concrete_type_id, ctx); - (name_fn(i), value) - }) - .collect() -} - fn is_tuple(type_long_id: &ConcreteTypeLongId) -> bool { type_long_id.generic_id.0 == "Struct" && matches!( @@ -269,48 +301,37 @@ fn is_bool(type_long_id: &ConcreteTypeLongId) -> bool { } fn is_panic_result(type_long_id: &ConcreteTypeLongId) -> bool { - if type_long_id.generic_id.0 == "Enum" - && let GenericArg::UserType(user_type) = &type_long_id.generic_args[0] - && user_type.debug_name.as_ref().is_some_and(|n| n.starts_with("core::panics::PanicResult")) - { - true - } else { - false - } + type_long_id.generic_id.0 == "Enum" + && matches!( + type_long_id.generic_args.first(), + Some(GenericArg::UserType(user_type)) + if user_type.debug_name.as_ref().is_some_and(|n| n.starts_with("core::panics::PanicResult")) + ) } -fn maybe_extract_felt_from_cell( +fn maybe_extract_maybe_relocatable_from_cell( cell: &CellExpression, registers_values: &RegistersValues, vm: &VirtualMachine, -) -> Option { +) -> Option { match cell { CellExpression::Deref(cell_ref) => { - maybe_get_felt_from_cell_ref(cell_ref, registers_values, vm) + let addr = registers_values.relocatable_from_cell_ref(cell_ref); + maybe_get_maybe_relocatable(addr, vm) } CellExpression::DoubleDeref(cell_ref, offset) => { - let relocatable = registers_values.relocatable_from_cell_ref(cell_ref); - - // [cell_ref] - let mut relocatable = match vm.segments.memory.get_relocatable(relocatable) { - Ok(value) => Some(value), + let addr = registers_values.relocatable_from_cell_ref(cell_ref); + let mut inner = match vm.segments.memory.get_relocatable(addr) { + Ok(value) => value, Err(err) => { error!("error when extracting relocatable from VM: {err:?}"); - None - } - }?; - - relocatable.offset = (relocatable.offset as isize + *offset as isize) as usize; - // [[cell_ref] + offset] - match vm.segments.memory.get_integer(relocatable) { - Ok(value) => Some(*value), - Err(err) => { - error!("error when extracting felt from VM: {err:?}"); - None + return None; } - } + }; + inner.offset = (inner.offset as isize + *offset as isize) as usize; + maybe_get_maybe_relocatable(inner, vm) } - CellExpression::Immediate(value) => Some(Felt::from(value)), + CellExpression::Immediate(value) => Some(MaybeRelocatable::Int(Felt252::from(value))), CellExpression::BinOp { op, a, b } => { let a_felt = maybe_get_felt_from_cell_ref(a, registers_values, vm)?; let b_felt = match b { @@ -319,17 +340,81 @@ fn maybe_extract_felt_from_cell( } DerefOrImmediate::Immediate(value) => Some(Felt::from(value.value.clone())), }?; - - Some(match op { + Some(MaybeRelocatable::Int(match op { CellOperator::Add => a_felt + b_felt, CellOperator::Sub => a_felt - b_felt, CellOperator::Mul => a_felt * b_felt, CellOperator::Div => a_felt.field_div(&NonZeroFelt::try_from(b_felt).unwrap()), - }) + })) } } } +fn extract_array_pointers(values: &[MaybeRelocatable]) -> Option<(Relocatable, Relocatable)> { + if values.len() != 2 { + warn!("expected 2 values for array/span, got {}", values.len()); + return None; + } + let MaybeRelocatable::RelocatableValue(start_ptr) = values[0] else { + warn!("expected relocatable for array start pointer"); + return None; + }; + let MaybeRelocatable::RelocatableValue(end_ptr) = values[1] else { + warn!("expected relocatable for array end pointer"); + return None; + }; + Some((start_ptr, end_ptr)) +} + +fn extract_array_from_pointers( + start_ptr: Relocatable, + end_ptr: Relocatable, + element_type_id: &ConcreteTypeId, + vm: &VirtualMachine, + ctx: &Context, +) -> Option { + if start_ptr.segment_index != end_ptr.segment_index { + warn!("array start and end pointers in different segments"); + return None; + } + + let element_size = ctx.type_size(element_type_id); + if element_size == 0 { + return Some(CairoValue::Array { elements: vec![] }); + } + + let total_cells = end_ptr.offset.checked_sub(start_ptr.offset)?; + if total_cells % element_size != 0 { + warn!("array size {total_cells} not divisible by element size {element_size}"); + return None; + } + + let num_elements = total_cells / element_size; + let mut elements = Vec::with_capacity(num_elements); + + for i in 0..num_elements { + let element_values = (0..element_size) + .map(|j| { + maybe_get_maybe_relocatable( + Relocatable { + segment_index: start_ptr.segment_index, + offset: start_ptr.offset + i * element_size + j, + }, + vm, + ) + }) + .collect::>>()?; + elements.push(maybe_relocatables_to_cairo_value( + &element_values, + element_type_id, + vm, + ctx, + )?); + } + + Some(CairoValue::Array { elements }) +} + fn maybe_get_felt_from_cell_ref( cell_ref: &CellRef, registers_values: &RegistersValues, @@ -344,3 +429,13 @@ fn maybe_get_felt_from_cell_ref( } } } + +fn maybe_get_maybe_relocatable(addr: Relocatable, vm: &VirtualMachine) -> Option { + match vm.segments.memory.get_maybe_relocatable(addr) { + Ok(value) => Some(value), + Err(err) => { + error!("error when reading memory at {addr}: {err:?}"); + None + } + } +}