Skip to content

Commit 19afecd

Browse files
committed
Add function level trace
commit-id:3a7228bd
1 parent 546c8a7 commit 19afecd

File tree

15 files changed

+358
-3
lines changed

15 files changed

+358
-3
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
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 & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,16 @@ 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
2022
starknet_api.workspace = true
21-
thiserror.workspace = true
23+
thiserror.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: 11 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, FunctionTraceError};
23
use crate::trace::types::{
34
CallerAddress, ContractAddress, ContractName, ContractTrace, Selector, TestName, TraceInfo,
45
TransformedCallResult, TransformedCalldata,
@@ -38,6 +39,7 @@ impl<'a> Collector<'a> {
3839
fn collect_contract_trace(&self) -> ContractTrace {
3940
let components = self.context.components();
4041
let entry_point = &self.call_trace.entry_point;
42+
4143
let nested_calls = self.collect_nested_calls();
4244
let contract_name = self.collect_contract_name();
4345
let abi = self.collect_abi();
@@ -52,6 +54,7 @@ impl<'a> Collector<'a> {
5254
call_type: components.call_type(entry_point.call_type),
5355
nested_calls,
5456
call_result: components.call_result_lazy(|| self.collect_transformed_call_result(abi)),
57+
function_trace: components.function_trace_lazy(|| self.collect_function_trace()),
5558
};
5659

5760
ContractTrace {
@@ -125,6 +128,14 @@ impl<'a> Collector<'a> {
125128
})
126129
}
127130

131+
fn collect_function_trace(&self) -> Result<FunctionTrace, FunctionTraceError> {
132+
FunctionTrace::create(
133+
*self.class_hash(),
134+
self.call_trace,
135+
self.contracts_data_store(),
136+
)
137+
}
138+
128139
fn class_hash(&self) -> &ClassHash {
129140
self.call_trace
130141
.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, FunctionTraceError};
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, Result<FunctionTrace, FunctionTraceError>);
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, FunctionTraceError};
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+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
use crate::contracts_data_store::ContractsDataStore;
2+
use crate::trace::function::name::FunctionName;
3+
use crate::trace::function::node::FunctionNode;
4+
use crate::trace::function::stack::CallStack;
5+
use cairo_annotations::map_pcs_to_sierra_statement_ids;
6+
use cairo_annotations::trace_data::{CasmLevelInfo, TraceEntry};
7+
use cairo_lang_sierra::extensions::core::{CoreConcreteLibfunc, CoreLibfunc, CoreType};
8+
use cairo_lang_sierra::program::{GenStatement, StatementIdx};
9+
use cairo_lang_sierra::program_registry::ProgramRegistry;
10+
use cheatnet::state::CallTrace;
11+
use starknet_api::core::ClassHash;
12+
13+
#[derive(Debug, Clone, thiserror::Error)]
14+
pub enum FunctionTraceError {
15+
#[error("function trace is not supported for forked contracts")]
16+
ForkContract,
17+
}
18+
/// `FunctionTrace` represents a trace of function calls in a Sierra program.
19+
/// It captures the structure of function calls and returns, allowing for analysis of the
20+
/// execution flow within a contract.
21+
#[derive(Debug, Clone)]
22+
pub struct FunctionTrace {
23+
pub root: FunctionNode,
24+
}
25+
26+
impl FunctionTrace {
27+
/// Creates a new [`FunctionTrace`] from the given [`ClassHash`], [`CallTrace`], and [`ContractsDataStore`].
28+
pub fn create(
29+
class_hash: ClassHash,
30+
call_trace: &CallTrace,
31+
contracts_data_store: &ContractsDataStore,
32+
) -> Result<Self, FunctionTraceError> {
33+
if contracts_data_store.is_fork(&class_hash) {
34+
return Err(FunctionTraceError::ForkContract);
35+
}
36+
let program_artifact = contracts_data_store
37+
.get_program_artifact(&class_hash)
38+
.expect("program artifact should be present for local contracts");
39+
40+
let program = &program_artifact.program;
41+
42+
let sierra_program_registry = ProgramRegistry::<CoreType, CoreLibfunc>::new(program)
43+
.expect("failed to create Sierra program registry");
44+
45+
let mut call_stack = CallStack::new();
46+
let mut stacks = Vec::new();
47+
48+
for statement_idx in get_sierra_statements(class_hash, call_trace, contracts_data_store) {
49+
let function_name = FunctionName::from_program(statement_idx, program);
50+
51+
let stack = call_stack.new_stack(function_name);
52+
53+
stacks.push(stack.clone());
54+
55+
let statement = program
56+
.statements
57+
.get(statement_idx.0)
58+
.expect("statement should be present");
59+
60+
match statement {
61+
GenStatement::Invocation(invocation) => {
62+
let libfunc = sierra_program_registry.get_libfunc(&invocation.libfunc_id);
63+
64+
if let Ok(CoreConcreteLibfunc::FunctionCall(_)) = &libfunc {
65+
call_stack.enter_function_call(stack);
66+
}
67+
}
68+
GenStatement::Return(_) => {
69+
call_stack.exit_function_call();
70+
}
71+
}
72+
}
73+
74+
Ok(build_function_trace(stacks))
75+
}
76+
}
77+
78+
/// Retrieves vector of [`StatementIdx`] from the given [`CallTrace`].
79+
fn get_sierra_statements(
80+
class_hash: ClassHash,
81+
call_trace: &CallTrace,
82+
contracts_data_store: &ContractsDataStore,
83+
) -> Vec<StatementIdx> {
84+
let casm_debug_info = contracts_data_store
85+
.get_casm_debug_info(&class_hash)
86+
.expect("Cairo program debug info should be present");
87+
88+
let casm_level_info = build_casm_level_info(call_trace);
89+
90+
map_pcs_to_sierra_statement_ids(casm_debug_info, &casm_level_info)
91+
.into_iter()
92+
.filter_map(Option::from)
93+
.collect()
94+
}
95+
96+
/// Builds a [`CasmLevelInfo`] from the given [`CallTrace`].
97+
fn build_casm_level_info(call_trace: &CallTrace) -> CasmLevelInfo {
98+
let vm_trace = call_trace
99+
.vm_trace
100+
.as_ref()
101+
.expect("vm trace should be present")
102+
.iter()
103+
.map(|value| TraceEntry {
104+
pc: value.pc,
105+
ap: value.ap,
106+
fp: value.fp,
107+
})
108+
.collect();
109+
110+
CasmLevelInfo {
111+
run_with_call_header: false,
112+
vm_trace,
113+
}
114+
}
115+
116+
/// Builds a [`FunctionTrace`] from the given stacks of function names.
117+
fn build_function_trace(stacks: Vec<Vec<FunctionName>>) -> FunctionTrace {
118+
let mut dummy_root = FunctionNode::new(FunctionName::NonInlined("root".to_string()));
119+
120+
for stack in stacks {
121+
dummy_root.add_path(stack);
122+
}
123+
124+
assert_eq!(dummy_root.children.len(), 1);
125+
let real_root = dummy_root
126+
.children
127+
.pop()
128+
.unwrap_or_else(|| unreachable!("assertion above should have prevented this"));
129+
130+
FunctionTrace { root: real_root }
131+
}

0 commit comments

Comments
 (0)