Skip to content

Commit 8a25e67

Browse files
committed
Add function level trace
commit-id:3a7228bd
1 parent d88921d commit 8a25e67

File tree

15 files changed

+350
-5
lines changed

15 files changed

+350
-5
lines changed

Cargo.lock

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/debugging/Cargo.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@ blockifier.workspace = true
88
cairo-lang-sierra.workspace = true
99
cairo-lang-sierra-to-casm.workspace = true
1010
cairo-lang-starknet-classes.workspace = true
11+
cairo-annotations.workspace = true
1112
cheatnet = { path = "../cheatnet" }
1213
console.workspace = true
1314
data-transformer = { path = "../data-transformer" }
1415
paste.workspace = true
1516
ptree.workspace = true
1617
rayon.workspace = true
18+
regex.workspace = true
1719
serde_json.workspace = true
1820
starknet-types-core.workspace = true
1921
starknet.workspace = true
20-
starknet_api.workspace = true
21-
thiserror.workspace = true
22+
starknet_api.workspace = true

crates/debugging/src/contracts_data_store.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,13 @@ impl ContractsDataStore {
122122
pub fn get_casm_debug_info(&self, class_hash: &ClassHash) -> Option<&CairoProgramDebugInfo> {
123123
self.casm_debug_infos.get(class_hash)
124124
}
125+
126+
/// Checks if the contract with the given [`ClassHash`] is a forked contract.
127+
pub fn is_fork(&self, class_hash: &ClassHash) -> bool {
128+
// We create contract names only from `ContractsData` and not from `ForkData`,
129+
// so if the contract name is not present in `contract_names`, it is a fork
130+
!self.contract_names.contains_key(class_hash)
131+
}
125132
}
126133

127134
/// Compile the given [`ContractClass`] to `casm` and return [`CairoProgramDebugInfo`]

crates/debugging/src/trace/collect.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::contracts_data_store::ContractsDataStore;
2+
use crate::trace::function::FunctionTrace;
23
use crate::trace::types::{
34
CallerAddress, ContractAddress, ContractName, ContractTrace, Selector, TestName, TraceInfo,
45
TransformedCallResult, TransformedCalldata,
@@ -12,6 +13,7 @@ use data_transformer::{reverse_transform_input, reverse_transform_output};
1213
use starknet::core::types::contract::AbiEntry;
1314
use starknet_api::core::ClassHash;
1415
use starknet_api::execution_utils::format_panic_data;
16+
use std::ops::Not;
1517

1618
pub struct Collector<'a> {
1719
call_trace: &'a CallTrace,
@@ -38,6 +40,7 @@ impl<'a> Collector<'a> {
3840
fn collect_contract_trace(&self) -> ContractTrace {
3941
let components = self.context.components();
4042
let entry_point = &self.call_trace.entry_point;
43+
4144
let nested_calls = self.collect_nested_calls();
4245
let contract_name = self.collect_contract_name();
4346
let abi = self.collect_abi();
@@ -52,6 +55,12 @@ impl<'a> Collector<'a> {
5255
call_type: components.call_type(entry_point.call_type),
5356
nested_calls,
5457
call_result: components.call_result_lazy(|| self.collect_transformed_call_result(abi)),
58+
#[expect(clippy::obfuscated_if_else)]
59+
function_trace: self
60+
.is_fork()
61+
.not()
62+
.then(|| components.function_trace_lazy(|| self.collect_function_trace()))
63+
.unwrap_or_default(),
5564
};
5665

5766
ContractTrace {
@@ -125,6 +134,18 @@ impl<'a> Collector<'a> {
125134
})
126135
}
127136

137+
fn collect_function_trace(&self) -> FunctionTrace {
138+
FunctionTrace::create(
139+
*self.class_hash(),
140+
self.call_trace,
141+
self.contracts_data_store(),
142+
)
143+
}
144+
145+
fn is_fork(&self) -> bool {
146+
self.contracts_data_store().is_fork(self.class_hash())
147+
}
148+
128149
fn class_hash(&self) -> &ClassHash {
129150
self.call_trace
130151
.entry_point

crates/debugging/src/trace/components.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::trace::function::FunctionTrace;
12
use crate::trace::types::{
23
CallerAddress, ContractAddress, ContractName, TransformedCallResult, TransformedCalldata,
34
};
@@ -42,6 +43,8 @@ pub enum Component {
4243
CallType,
4344
/// The result of the call, transformed for display.
4445
CallResult,
46+
/// The function trace, will be only collected if the contract is not a fork.
47+
FunctionTrace,
4548
}
4649

4750
macro_rules! impl_component_container {
@@ -58,7 +61,7 @@ macro_rules! impl_component_container {
5861
"` that is computed only if `Component::", stringify!($variant),
5962
"` is included in the [`Components`]."
6063
)]
61-
#[derive(Debug, Clone)]
64+
#[derive(Debug, Clone, Default)]
6265
pub struct [<$variant Container>] {
6366
value: Option<$ty>,
6467
}
@@ -122,3 +125,4 @@ impl_component_container!(ContractAddress);
122125
impl_component_container!(CallerAddress);
123126
impl_component_container!(CallType);
124127
impl_component_container!(CallResult, TransformedCallResult);
128+
impl_component_container!(FunctionTrace);
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
mod name;
2+
mod node;
3+
mod stack;
4+
mod trace;
5+
6+
pub use node::FunctionNode;
7+
pub use trace::FunctionTrace;
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
use cairo_lang_sierra::program::{Program, StatementIdx};
2+
use regex::Regex;
3+
use std::sync::LazyLock;
4+
5+
/// Represents a function name in the Sierra program.
6+
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
7+
pub enum FunctionName {
8+
NonInlined(String),
9+
// TODO: Add inlined variant in next PR.
10+
}
11+
12+
impl FunctionName {
13+
/// Creates a [`FunctionName`] from a [`StatementIdx`] index and a [`Program`].
14+
pub fn from_program(statement_idx: StatementIdx, program: &Program) -> Self {
15+
let function_idx = program
16+
.funcs
17+
.partition_point(|f| f.entry_point.0 <= statement_idx.0)
18+
- 1;
19+
let function_name = program.funcs[function_idx].id.to_string();
20+
let function_name = remove_loop_suffix(&function_name);
21+
let function_name = remove_monomorphization_suffix(&function_name);
22+
FunctionName::NonInlined(function_name)
23+
}
24+
25+
/// Returns the function name as a [`&str`].
26+
pub fn function_name(&self) -> &str {
27+
match self {
28+
FunctionName::NonInlined(name) => name,
29+
}
30+
}
31+
}
32+
33+
/// Remove suffix in case of loop function e.g. `[expr36]`.
34+
fn remove_loop_suffix(function_name: &str) -> String {
35+
static RE_LOOP_FUNC: LazyLock<Regex> = LazyLock::new(|| {
36+
Regex::new(r"\[expr\d*]")
37+
.expect("Failed to create regex for normalizing loop function names")
38+
});
39+
RE_LOOP_FUNC.replace(function_name, "").to_string()
40+
}
41+
42+
/// Remove parameters from monomorphised Cairo generics e.g. `<felt252>`.
43+
fn remove_monomorphization_suffix(function_name: &str) -> String {
44+
static RE_MONOMORPHIZATION: LazyLock<Regex> = LazyLock::new(|| {
45+
Regex::new(r"::<.*>")
46+
.expect("Failed to create regex for normalizing monomorphized generic function names")
47+
});
48+
RE_MONOMORPHIZATION.replace(function_name, "").to_string()
49+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
use crate::trace::function::name::FunctionName;
2+
3+
/// Represents a node in the function call tree, where each node corresponds to a function name
4+
/// and can have child nodes representing nested function calls.
5+
#[derive(Debug, Clone)]
6+
pub struct FunctionNode {
7+
pub value: FunctionName,
8+
pub children: Vec<FunctionNode>,
9+
}
10+
11+
impl FunctionNode {
12+
/// Creates a new [`FunctionNode`] with the given [`FunctionName`].
13+
pub fn new(value: FunctionName) -> Self {
14+
FunctionNode {
15+
value,
16+
children: Vec::new(),
17+
}
18+
}
19+
20+
/// Adds a path of function names to the current node, creating child nodes as necessary.
21+
pub fn add_path(&mut self, path: Vec<FunctionName>) {
22+
self.add_path_recursive(&mut path.into_iter());
23+
}
24+
25+
fn add_path_recursive(&mut self, iter: &mut impl Iterator<Item = FunctionName>) {
26+
if let Some(next) = iter.next() {
27+
if let Some(child) = self.children.iter_mut().find(|c| c.value == next) {
28+
child.add_path_recursive(iter);
29+
} else {
30+
let mut new_child = FunctionNode::new(next);
31+
new_child.add_path_recursive(iter);
32+
self.children.push(new_child);
33+
}
34+
}
35+
}
36+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
use crate::trace::function::name::FunctionName;
2+
3+
/// Represents a call stack for function calls, allowing to track the current function call
4+
pub struct CallStack {
5+
stack: Vec<FunctionName>,
6+
previous_stack_lengths: Vec<usize>,
7+
}
8+
9+
impl CallStack {
10+
/// Creates a new empty [`CallStack`]
11+
pub fn new() -> Self {
12+
Self {
13+
stack: Vec::new(),
14+
previous_stack_lengths: Vec::new(),
15+
}
16+
}
17+
18+
/// Enters a new function call by updating the stack with the new call stack.
19+
/// It saves the current stack length to allow returning to it later.
20+
///
21+
/// The New call stack is expected to be a prefix of the current stack.
22+
pub fn enter_function_call(&mut self, new_call_stack: Vec<FunctionName>) {
23+
self.previous_stack_lengths.push(self.stack.len());
24+
25+
self.stack = new_call_stack;
26+
}
27+
28+
/// Exits the current function call by truncating the stack to the previous length.
29+
/// If there is no previous length, it does nothing.
30+
pub fn exit_function_call(&mut self) {
31+
if let Some(previous_stack_len) = self.previous_stack_lengths.pop() {
32+
self.stack.truncate(previous_stack_len);
33+
}
34+
}
35+
36+
/// Creates new stack with the given function name.
37+
pub fn new_stack(&self, function_name: FunctionName) -> Vec<FunctionName> {
38+
let mut stack = self.stack.clone();
39+
40+
let empty_or_different_function = self.stack.last().is_none_or(|current_function| {
41+
current_function.function_name() != function_name.function_name()
42+
});
43+
44+
if empty_or_different_function {
45+
stack.push(function_name);
46+
}
47+
48+
stack
49+
}
50+
}

0 commit comments

Comments
 (0)