diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d8cca03..fe9758b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: - run: cargo test --release check: - name: Check formatting, linter and unused dependencies + name: Check formatting, linter, docs and unused dependencies runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 diff --git a/Cargo.lock b/Cargo.lock index d9d7b25..4ba95a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -152,18 +152,16 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cairo-annotations" version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e954fe93c0783b0f314977abbc9a94799cc4f217c2873616db8cdb368e036de" +source = "git+https://github.com/software-mansion/cairo-annotations?rev=ff307d9f0ab4c6514b90698e3ce29d91f8e0ce56#ff307d9f0ab4c6514b90698e3ce29d91f8e0ce56" dependencies = [ "cairo-lang-sierra", "cairo-lang-sierra-to-casm", "cairo-lang-sierra-type-size", "camino", "derive_more", - "regex", "serde", "serde_json", - "starknet-types-core", + "starknet-types-core 1.0.0", "strum", "strum_macros", "thiserror 2.0.18", @@ -184,7 +182,7 @@ dependencies = [ "indexmap", "scarb-metadata", "serde_json", - "starknet-types-core", + "starknet-types-core 0.2.4", "tracing", ] @@ -234,7 +232,7 @@ dependencies = [ "serde_json", "sha3", "smol_str", - "starknet-types-core", + "starknet-types-core 0.2.4", "thiserror 2.0.18", ] @@ -287,7 +285,7 @@ dependencies = [ "itertools", "num-bigint", "num-traits", - "starknet-types-core", + "starknet-types-core 0.2.4", "thiserror 2.0.18", ] @@ -347,7 +345,7 @@ dependencies = [ "sha2", "sha3", "starknet-crypto", - "starknet-types-core", + "starknet-types-core 0.2.4", "thiserror 2.0.18", "tracing", "zip", @@ -1682,7 +1680,7 @@ dependencies = [ "rfc6979", "sha2", "starknet-curve", - "starknet-types-core", + "starknet-types-core 0.2.4", "zeroize", ] @@ -1692,7 +1690,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22c898ae81b6409532374cf237f1bd752d068b96c6ad500af9ebbd0d9bb712f6" dependencies = [ - "starknet-types-core", + "starknet-types-core 0.2.4", ] [[package]] @@ -1715,6 +1713,23 @@ dependencies = [ "zeroize", ] +[[package]] +name = "starknet-types-core" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a12690813e587969cb4a9e7d8ebdb069d4bb7ec8d03275c5f719310c8e1f07c" +dependencies = [ + "generic-array", + "lambdaworks-crypto", + "lambdaworks-math", + "num-bigint", + "num-integer", + "num-traits", + "rand 0.9.2", + "serde", + "zeroize", +] + [[package]] name = "string_cache" version = "0.8.9" diff --git a/Cargo.toml b/Cargo.toml index 1c211cd..f84badc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ edition = "2024" [dependencies] anyhow = "1.0" -cairo-annotations = { version = "0.8.0", features = ["cairo-lang"] } +cairo-annotations = { git = "https://github.com/software-mansion/cairo-annotations", rev = "ff307d9f0ab4c6514b90698e3ce29d91f8e0ce56", features = ["cairo-lang"] } cairo-lang-casm = "2.17.0" cairo-lang-sierra = "2.17.0" cairo-lang-sierra-to-casm = "2.17.0" diff --git a/src/debugger/context.rs b/src/debugger/context.rs index 62916be..2d63d64 100644 --- a/src/debugger/context.rs +++ b/src/debugger/context.rs @@ -11,8 +11,11 @@ use cairo_annotations::annotations::debugger::DebuggerAnnotationsV1 as Functions use cairo_annotations::annotations::profiler::{ FunctionName, ProfilerAnnotationsV1 as SierraFunctionNames, }; +use cairo_annotations::annotations::type_names::{ + EnumInfo, SierraTypeId, StructInfo, TypeNamesAnnotationsV1 as TypeNames, +}; use cairo_annotations::{MappingResult, map_pc_to_sierra_statement_id}; -use cairo_lang_sierra::extensions::core::CoreConcreteLibfunc; +use cairo_lang_sierra::extensions::core::{CoreConcreteLibfunc, CoreTypeConcrete}; use cairo_lang_sierra::extensions::lib_func::BranchSignature; use cairo_lang_sierra::extensions::types::TypeInfo; use cairo_lang_sierra::extensions::{ConcreteLibfunc, ConcreteType}; @@ -52,6 +55,7 @@ struct SierraContext { program_registry_info: ProgramRegistryInfo, code_locations: SierraCodeLocations, function_names: SierraFunctionNames, + type_names: Option, } impl Context { @@ -75,6 +79,7 @@ impl Context { let function_names = SierraFunctionNames::try_from_debug_info(&debug_info).context( "statements functions debug info is missing - enable generating it in your Scarb.toml", )?; + let type_names = TypeNames::try_from_debug_info(&debug_info).ok(); // TODO(#61) let casm_debug_info = @@ -87,8 +92,13 @@ impl Context { #[cfg(feature = "dev")] let labels = readable_sierra_ids::extract_labels(&program); - let sierra_context = - SierraContext { program, program_registry_info, code_locations, function_names }; + let sierra_context = SierraContext { + program, + program_registry_info, + code_locations, + function_names, + type_names, + }; Ok(Self { #[cfg(feature = "dev")] @@ -233,14 +243,25 @@ impl Context { &branch_signature.vars[var_index].ty } - #[expect(dead_code)] - pub fn type_size(&self, type_id: &ConcreteTypeId) -> i16 { + pub fn type_size(&self, type_id: &ConcreteTypeId) -> usize { *self .sierra_context .program_registry_info .type_sizes .get(type_id) - .expect("type id is expected to exist in type size map") + .expect("type id is expected to exist in type size map") as usize + } + + pub fn get_concrete_type(&self, type_id: &ConcreteTypeId) -> Option<&CoreTypeConcrete> { + self.sierra_context.program_registry_info.registry().get_type(type_id).ok() + } + + pub fn struct_info(&self, type_id: &ConcreteTypeId) -> Option<&StructInfo> { + self.sierra_context.type_names.as_ref()?.structs.get(&SierraTypeId(type_id.id)) + } + + pub fn enum_info(&self, type_id: &ConcreteTypeId) -> Option<&EnumInfo> { + self.sierra_context.type_names.as_ref()?.enums.get(&SierraTypeId(type_id.id)) } fn statement_idx_to_statement(&self, statement_idx: StatementIdx) -> &Statement { diff --git a/src/debugger/handler.rs b/src/debugger/handler.rs index ad9236b..32dbdb0 100644 --- a/src/debugger/handler.rs +++ b/src/debugger/handler.rs @@ -13,7 +13,7 @@ use tracing::{error, trace}; use crate::debugger::MAX_OBJECT_REFERENCE; use crate::debugger::context::{Context, Line}; -use crate::debugger::state::{RequestedVariables, State}; +use crate::debugger::state::State; pub struct HandlerResponse { pub response_body: ResponseBody, @@ -154,11 +154,7 @@ pub fn handle_request( Ok(ResponseBody::Scopes(ScopesResponse { scopes }).into()) } Command::Variables(VariablesArguments { variables_reference, .. }) => { - let variables = state.call_stack.get_variables( - RequestedVariables::VariablesReference(*variables_reference), - ctx, - vm, - ); + let variables = state.call_stack.get_variables(*variables_reference, ctx, vm); Ok(ResponseBody::Variables(VariablesResponse { variables }).into()) } diff --git a/src/debugger/state.rs b/src/debugger/state.rs index d340ceb..58db7d5 100644 --- a/src/debugger/state.rs +++ b/src/debugger/state.rs @@ -18,8 +18,6 @@ type SourcePath = String; mod call_stack; mod ui_state; -pub use call_stack::RequestedVariables; - pub struct State { configuration_done: bool, execution_stopped: bool, diff --git a/src/debugger/state/call_stack.rs b/src/debugger/state/call_stack.rs index 29c940f..aff04c5 100644 --- a/src/debugger/state/call_stack.rs +++ b/src/debugger/state/call_stack.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::iter; use std::path::Path; @@ -17,9 +18,13 @@ use crate::debugger::state::call_stack::variables::get_values_of_variables; mod variables; +pub use variables::CairoValue; + type SubStack = Vec<(StackFrame, FunctionVariables)>; -#[derive(Default)] +/// Nested variable references start above any realistic frame scope reference value. +const NESTED_VAR_REF_START: i64 = 100_000; + pub struct CallStack { /// Stack of Cairo function frames and values of variables in frames corresponding /// to these functions. @@ -55,6 +60,28 @@ pub struct CallStack { /// statement maps to a function call or a return statement. /// The stack should be modified ***after*** such a statement is executed. action_on_new_statement: Option, + + /// Registry mapping DAP `variables_reference` values to their child variables. + /// Used for expanding nested struct/enum values in the IDE. + /// Only entries for the current function frame are cleared on each new Sierra statement. + nested_var_registry: HashMap>, + + /// Counter for assigning nested variable reference IDs. + /// Starts at [`NESTED_VAR_REF_START`] and is reset to the current frame's boundary each step, + /// reclaiming IDs for the current frame (uniqueness is only required within a stopped state). + next_nested_ref: i64, +} + +impl Default for CallStack { + fn default() -> Self { + Self { + call_frames_and_vars: Default::default(), + current_sierra_function_context: Default::default(), + action_on_new_statement: Default::default(), + nested_var_registry: Default::default(), + next_nested_ref: NESTED_VAR_REF_START, + } + } } impl CallStack { @@ -74,9 +101,14 @@ impl CallStack { std::mem::take(&mut self.current_sierra_function_context); self.call_frames_and_vars.push((frames_and_variables, sierra_function_context)); + self.current_sierra_function_context.nested_ref_start = self.next_nested_ref; } Some(Action::Pop) => { if let Some((_, sierra_function_context)) = self.call_frames_and_vars.pop() { + let start = sierra_function_context.nested_ref_start; + self.nested_var_registry.retain(|&k, _| k < start); + self.next_nested_ref = start; + self.current_sierra_function_context = sierra_function_context; } } @@ -95,19 +127,22 @@ impl CallStack { return; } + // Invalidate only the current function's entries: keys below nested_ref_start + // belong to past (caller) frames whose variables are fixed and can remain cached. + // Reset the counter to reclaim those IDs — uniqueness is only required within a single + // stopped state, not across the whole session. + let start = self.current_sierra_function_context.nested_ref_start; + self.nested_var_registry.retain(|&k, _| k < start); + self.next_nested_ref = start; + self.current_sierra_function_context.handle_branch_entrances(statement_idx, ctx, vm); self.current_sierra_function_context.last_executed_statement = Some(statement_idx); if ctx.is_function_call_statement(statement_idx) { let frames: Vec<_> = self.build_stack_frames(ctx, statement_idx).collect(); // TODO(#95): handle variables of inlined functions. - let vars = iter::repeat_n(FunctionVariables::default(), frames.len() - 1).chain( - iter::once(get_values_of_variables( - ctx, - vm, - &self.current_sierra_function_context.post_statements_registers, - )), - ); + let vars = iter::repeat_n(FunctionVariables::default(), frames.len() - 1) + .chain(iter::once(self.get_current_function_variables(ctx, vm))); let frames_and_vars = frames.into_iter().zip(vars).collect(); @@ -139,25 +174,38 @@ impl CallStack { vec![scope] } - pub fn get_variables( + /// Returns the semantic variable values for the current function without modifying + /// the nested variable registry or incrementing reference counters. + pub fn get_current_function_variables( &self, - requested_variables: RequestedVariables, + ctx: &Context, + vm: &VirtualMachine, + ) -> FunctionVariables { + get_values_of_variables( + ctx, + vm, + &self.current_sierra_function_context.post_statements_registers, + ) + } + + pub fn get_variables( + &mut self, + variables_reference: i64, ctx: &Context, vm: &VirtualMachine, ) -> Vec { - let flat_index = match requested_variables { - RequestedVariables::CurrentFunction => self.flat_length(), - RequestedVariables::VariablesReference(variables_reference) => { - (variables_reference / 2 - 1) as usize - } - }; + // Check the nested registry first (handles expansion of struct/enum children). + if let Some(children) = self.nested_var_registry.get(&variables_reference).cloned() { + return children + .into_iter() + .map(|(name, value)| self.cairo_value_to_variable(name, value)) + .collect(); + } + + let flat_index = (variables_reference / 2 - 1) as usize; let FunctionVariables { names_to_values } = if flat_index >= self.flat_length() { - get_values_of_variables( - ctx, - vm, - &self.current_sierra_function_context.post_statements_registers, - ) + self.get_current_function_variables(ctx, vm) } else { self.call_frames_and_vars .iter() @@ -170,13 +218,59 @@ impl CallStack { names_to_values .into_iter() - .map(|(name, value)| Variable { + .map(|(name, value)| self.cairo_value_to_variable(name, value)) + .collect() + } + + fn cairo_value_to_variable(&mut self, name: String, value: CairoValue) -> Variable { + match value { + CairoValue::FeltLike(value) => Variable { name, - value, + value: value.to_string(), variables_reference: 0, ..Default::default() - }) - .collect() + }, + CairoValue::Struct { type_name, fields } => { + if fields.is_empty() { + Variable { + name, + value: "()".to_string(), + variables_reference: 0, + ..Default::default() + } + } else { + let ref_id = self.register_children(fields); + Variable { + name, + value: type_name, + variables_reference: ref_id, + ..Default::default() + } + } + } + CairoValue::Enum { type_name, variant_name, variant_value } => { + let display_name = format!("{type_name}::{variant_name}"); + let ref_id = self.register_children(vec![("value".to_string(), *variant_value)]); + Variable { + name, + value: display_name, + variables_reference: ref_id, + ..Default::default() + } + } + CairoValue::Other(value) => { + Variable { name, value, variables_reference: 0, ..Default::default() } + } + } + } + + fn register_children(&mut self, children: Vec<(String, CairoValue)>) -> i64 { + let ref_id = self.next_nested_ref; + self.nested_var_registry.insert(ref_id, children); + + self.next_nested_ref += 1; + + ref_id } /// Builds a vector of stack frames, ordered from the least nested to the most nested element. @@ -260,7 +354,6 @@ impl CallStack { type PostStatementsRegisters = IndexMap, RegistersValues)>; -#[derive(Default)] struct SierraFunctionContext { /// Mapping from sierra statements executed during the function frame execution to values of registers /// right after entering any branch of the statement (by definition only one branch is entered) @@ -274,6 +367,20 @@ struct SierraFunctionContext { post_statements_registers: PostStatementsRegisters, last_executed_statement: Option, + + /// The lowest `nested_var_registry` key that belongs to this function frame. + /// Entries with keys >= this value are cleared when execution moves to a new statement. + nested_ref_start: i64, +} + +impl Default for SierraFunctionContext { + fn default() -> Self { + Self { + post_statements_registers: Default::default(), + last_executed_statement: Default::default(), + nested_ref_start: NESTED_VAR_REF_START, + } + } } impl SierraFunctionContext { @@ -360,14 +467,9 @@ enum Action { Pop, } -#[derive(Default, Clone)] -struct FunctionVariables { - names_to_values: IndexMap, -} - -pub enum RequestedVariables { - CurrentFunction, - VariablesReference(i64), +#[derive(Default, Clone, PartialEq)] +pub struct FunctionVariables { + names_to_values: IndexMap, } #[derive(Clone, Debug)] diff --git a/src/debugger/state/call_stack/variables.rs b/src/debugger/state/call_stack/variables.rs index ef5ba4e..be8b899 100644 --- a/src/debugger/state/call_stack/variables.rs +++ b/src/debugger/state/call_stack/variables.rs @@ -1,7 +1,8 @@ use cairo_annotations::annotations::coverage::SourceCodeSpan; use cairo_lang_casm::cell_expression::{CellExpression, CellOperator}; use cairo_lang_casm::operand::{CellRef, DerefOrImmediate}; -use cairo_lang_sierra::ids::VarId; +use cairo_lang_sierra::extensions::core::CoreTypeConcrete; +use cairo_lang_sierra::ids::ConcreteTypeId; use cairo_lang_sierra::program::{ConcreteTypeLongId, GenericArg}; use cairo_vm::Felt252; use cairo_vm::vm::vm_core::VirtualMachine; @@ -14,12 +15,20 @@ use crate::debugger::state::call_stack::{ FunctionVariables, PostStatementsRegisters, RegistersValues, }; +#[derive(Clone, Debug, PartialEq)] +pub enum CairoValue { + FeltLike(Felt252), + Struct { type_name: String, fields: Vec<(String, CairoValue)> }, + Enum { type_name: String, variant_name: String, variant_value: Box }, + Other(String), +} + pub fn get_values_of_variables( ctx: &Context, 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 { @@ -52,7 +61,13 @@ pub fn get_values_of_variables( .filter_map(|cell| maybe_extract_felt_from_cell(cell, registers_values, vm)) .collect(); - if let Some((curr_span, _, _)) = current_var_values.get(name) { + // 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() { + continue; + } + + if let Some((curr_span, _, curr_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 @@ -61,13 +76,21 @@ pub fn get_values_of_variables( { continue; } + // TODO(#136) + // 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() { + continue; + } } + // TODO(#128): fix unit type mappings if cells_vals.is_empty() { continue; } - current_var_values.insert(name.clone(), (span.clone(), var_id.clone(), cells_vals)); + current_var_values.insert(name.clone(), (span.clone(), type_id, cells_vals)); } // TODO(#99): drop consumed values. @@ -75,27 +98,105 @@ pub fn get_values_of_variables( let names_to_values = current_var_values .into_iter() - .filter_map(|(name, (loc, var_id, value_in_felts))| { - if value_in_felts.len() == 1 { - Some((name, value_in_felts[0].to_string())) - } else { - warn!("unsupported value: ({name}, {loc:?}) {var_id:?} {value_in_felts:?}"); - None - } + .map(|(name, (_, type_id, felts))| { + let value = felts_to_cairo_value(&felts, &type_id, ctx); + (name, value) }) .collect(); FunctionVariables { names_to_values } } +fn felts_to_cairo_value(felts: &[Felt252], type_id: &ConcreteTypeId, ctx: &Context) -> CairoValue { + let Some(concrete_type) = ctx.get_concrete_type(type_id) else { + return fallback_value(felts, type_id); + }; + + match concrete_type { + CoreTypeConcrete::Struct(struct_type) => { + 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 fields = struct_type + .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); + let field_name = struct_info + .and_then(|info| info.members.get(i)) + .cloned() + .unwrap_or_else(|| format!(".{i}")); + (field_name, value) + }) + .collect(); + CairoValue::Struct { type_name, fields } + } + CoreTypeConcrete::Enum(enum_type) => { + 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] + .as_ref() + .to_biguint() + .try_into() + .expect("enum discriminant larger than usize::MAX"); + // 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. + let variant_index = if n_variants <= 2 { + stored_discriminant + } else { + n_variants.saturating_sub(stored_discriminant.div_ceil(2)) + }; + + 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); + 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) } + } + _ => fallback_value(felts, 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 extract_short_type_name(type_id: &ConcreteTypeId) -> String { + if let Some(debug_name) = &type_id.debug_name { + // Strip generic parameters and take the last path segment. + let base = debug_name.split('<').next().unwrap_or(debug_name.as_str()); + return base.split("::").last().unwrap_or(base).to_string(); + } + format!("type_{}", type_id.id) +} + 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] - // `core::panics::PanicResult` always has a debug name for some reason. - && user_type - .debug_name - .clone() - .is_some_and(|x| x.starts_with("core::panics::PanicResult")) + && user_type.debug_name.as_ref().is_some_and(|n| n.starts_with("core::panics::PanicResult")) { true } else { diff --git a/src/debugger/state/ui_state.rs b/src/debugger/state/ui_state.rs index 312f3b2..cc788a1 100644 --- a/src/debugger/state/ui_state.rs +++ b/src/debugger/state/ui_state.rs @@ -1,23 +1,22 @@ use cairo_vm::vm::vm_core::VirtualMachine; -use dap::types::{StackFrame, Variable}; +use dap::types::StackFrame; use crate::debugger::context::Context; use crate::debugger::state::State; -use crate::debugger::state::call_stack::RequestedVariables; +use crate::debugger::state::call_stack::FunctionVariables; /// Represents the state of the debugger from the user's point of view. /// E.g. `stack_trace` is visible to a user through [`dap::prelude::Command::StackTrace`] request. #[derive(PartialEq)] pub struct UiState { stack_trace: Vec, - values_of_variables: Vec, + values_of_variables: FunctionVariables, } impl UiState { pub fn build(state: &State, ctx: &Context, vm: &VirtualMachine) -> Self { let stack_trace = state.call_stack.get_frames(state.current_statement_idx, ctx); - let values_of_variables = - state.call_stack.get_variables(RequestedVariables::CurrentFunction, ctx, vm); + let values_of_variables = state.call_stack.get_current_function_variables(ctx, vm); UiState { stack_trace, values_of_variables } } }