Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
5ed6cda
initial static cost walk
tippenein Oct 15, 2025
7b6ff35
use SymbolicExpression and change to using NativeFunctions
tippenein Oct 15, 2025
9d5bba6
remove branching attribute
tippenein Oct 15, 2025
54d0e97
actually parse into SE instead of PSE
tippenein Oct 15, 2025
06cdea6
use NativeFunctions and attempt cost lookup
brady-stacks Oct 20, 2025
64dbb3e
fix NativeFunction matching
brady-stacks Oct 20, 2025
61abc75
properly build function definition top-level map
brady-stacks Oct 21, 2025
84d1789
2-pass for dependent function costs
brady-stacks Oct 22, 2025
53bbf6c
use Environment for static_cost
brady-stacks Oct 28, 2025
825d4ac
return the cost analysis node root in static_cost_tree
brady-stacks Nov 4, 2025
71e49be
attempt at trait counting walk
brady-stacks Nov 20, 2025
c0650f6
visitor pattern cleanup
brady-stacks Nov 21, 2025
4ada295
trait counting on map/filter/fold minimum to zero
brady-stacks Nov 21, 2025
50b007b
replace deprecated InterpreterResult usages
brady-stacks Nov 21, 2025
ea0c605
test larger contract analysis with pox-4
brady-stacks Nov 22, 2025
2df940b
clean up modules and simplify listlike builder
brady-stacks Nov 24, 2025
8e499d6
move static_cost_native to test only
brady-stacks Nov 24, 2025
4dffab2
make static_cost functions return trait counts also
brady-stacks Nov 24, 2025
9e194d6
delegate to correct cost modules
brady-stacks Nov 25, 2025
d2cc5de
gate static costs behind developer-mode
brady-stacks Nov 25, 2025
2c3dca4
use lookup_reserved_functions
brady-stacks Nov 26, 2025
df8e4ad
pass epoch along for cost eval
brady-stacks Nov 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions clarity/src/vm/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
pub mod definition_sorter;
pub mod expression_identifier;
pub mod parser;
#[cfg(feature = "developer-mode")]
pub mod static_cost;
pub mod traits_resolver;

pub mod errors;
Expand Down
223 changes: 223 additions & 0 deletions clarity/src/vm/ast/static_cost/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
mod trait_counter;
use std::collections::HashMap;

use clarity_types::types::{CharType, SequenceData};
use stacks_common::types::StacksEpochId;
pub use trait_counter::{
TraitCount, TraitCountCollector, TraitCountContext, TraitCountPropagator, TraitCountVisitor,
};

use crate::vm::callables::CallableType;
use crate::vm::costs::analysis::{
CostAnalysisNode, CostExprNode, StaticCost, SummingExecutionCost,
};
use crate::vm::costs::cost_functions::linear;
use crate::vm::costs::costs_1::Costs1;
use crate::vm::costs::costs_2::Costs2;
use crate::vm::costs::costs_3::Costs3;
use crate::vm::costs::costs_4::Costs4;
use crate::vm::costs::ExecutionCost;
use crate::vm::errors::VmExecutionError;
use crate::vm::functions::{lookup_reserved_functions, NativeFunctions};
use crate::vm::representations::ClarityName;
use crate::vm::{ClarityVersion, Value};

const STRING_COST_BASE: u64 = 36;
const STRING_COST_MULTIPLIER: u64 = 3;

pub(crate) fn calculate_function_cost(
function_name: String,
cost_map: &HashMap<String, Option<StaticCost>>,
_clarity_version: &ClarityVersion,
) -> Result<StaticCost, String> {
match cost_map.get(&function_name) {
Some(Some(cost)) => {
// Cost already computed
Ok(cost.clone())
}
Some(None) => {
// Should be impossible..
// Function exists but cost not yet computed, circular dependency?
// For now, return zero cost to avoid infinite recursion
println!(
"Circular dependency detected for function: {}",
function_name
);
Ok(StaticCost::ZERO)
}
None => {
// Function not found
Ok(StaticCost::ZERO)
}
}
}

/// Determine if a function name represents a branching function
pub(crate) fn is_branching_function(function_name: &ClarityName) -> bool {
match function_name.as_str() {
"if" | "match" => true,
"unwrap!" | "unwrap-err!" => false, // XXX: currently unwrap and
// unwrap-err traverse both branches regardless of result, so until this is
// fixed in clarity we'll set this to false
_ => false,
}
}

pub(crate) fn is_node_branching(node: &CostAnalysisNode) -> bool {
match &node.expr {
CostExprNode::NativeFunction(NativeFunctions::If)
| CostExprNode::NativeFunction(NativeFunctions::Match) => true,
CostExprNode::UserFunction(name) => is_branching_function(name),
_ => false,
}
}

/// string cost based on length
fn string_cost(length: usize) -> StaticCost {
let cost = linear(length as u64, STRING_COST_BASE, STRING_COST_MULTIPLIER);
let execution_cost = ExecutionCost::runtime(cost);
StaticCost {
min: execution_cost.clone(),
max: execution_cost,
}
}

/// Strings are the only Value's with costs associated
pub(crate) fn calculate_value_cost(value: &Value) -> Result<StaticCost, String> {
match value {
Value::Sequence(SequenceData::String(CharType::UTF8(data))) => {
Ok(string_cost(data.data.len()))
}
Value::Sequence(SequenceData::String(CharType::ASCII(data))) => {
Ok(string_cost(data.data.len()))
}
_ => Ok(StaticCost::ZERO),
}
}

pub(crate) fn calculate_function_cost_from_native_function(
native_function: NativeFunctions,
arg_count: u64,
epoch: StacksEpochId,
) -> Result<StaticCost, String> {
// Derive clarity_version from epoch for lookup_reserved_functions
let clarity_version = ClarityVersion::default_for_epoch(epoch);
let cost_function =
match lookup_reserved_functions(native_function.to_string().as_str(), &clarity_version) {
Some(CallableType::NativeFunction(_, _, cost_fn)) => cost_fn,
Some(CallableType::NativeFunction205(_, _, cost_fn, _)) => cost_fn,
Some(CallableType::SpecialFunction(_, _)) => return Ok(StaticCost::ZERO),
Some(CallableType::UserFunction(_)) => return Ok(StaticCost::ZERO), // TODO ?
None => {
return Ok(StaticCost::ZERO);
}
};

let cost = match epoch {
StacksEpochId::Epoch20 => cost_function.eval::<Costs1>(arg_count),
StacksEpochId::Epoch2_05 => cost_function.eval::<Costs2>(arg_count),
StacksEpochId::Epoch21
| StacksEpochId::Epoch22
| StacksEpochId::Epoch23
| StacksEpochId::Epoch24
| StacksEpochId::Epoch25
| StacksEpochId::Epoch30
| StacksEpochId::Epoch31
| StacksEpochId::Epoch32 => cost_function.eval::<Costs3>(arg_count),
StacksEpochId::Epoch33 => cost_function.eval::<Costs4>(arg_count),
StacksEpochId::Epoch10 => {
// fallback to costs 1 since epoch 1 doesn't have direct cost mapping
cost_function.eval::<Costs1>(arg_count)
}
}
.map_err(|e| format!("Cost calculation error: {:?}", e))?;
Ok(StaticCost {
min: cost.clone(),
max: cost,
})
}

/// total cost handling branching
pub(crate) fn calculate_total_cost_with_summing(node: &CostAnalysisNode) -> SummingExecutionCost {
let mut summing_cost = SummingExecutionCost::from_single(node.cost.min.clone());

for child in &node.children {
let child_summing = calculate_total_cost_with_summing(child);
summing_cost.add_summing(&child_summing);
}

summing_cost
}

pub(crate) fn calculate_total_cost_with_branching(node: &CostAnalysisNode) -> SummingExecutionCost {
let mut summing_cost = SummingExecutionCost::new();

// Check if this is a branching function by examining the node's expression
let is_branching = is_node_branching(node);

if is_branching {
match &node.expr {
CostExprNode::NativeFunction(NativeFunctions::If)
| CostExprNode::NativeFunction(NativeFunctions::Match) => {
// TODO match?
if node.children.len() >= 2 {
let condition_cost = calculate_total_cost_with_summing(&node.children[0]);
let condition_total = condition_cost.add_all();

// Add the root cost + condition cost to each branch
let mut root_and_condition = node.cost.min.clone();
let _ = root_and_condition.add(&condition_total);

for child_cost_node in node.children.iter().skip(1) {
let branch_cost = calculate_total_cost_with_summing(child_cost_node);
let branch_total = branch_cost.add_all();

let mut path_cost = root_and_condition.clone();
let _ = path_cost.add(&branch_total);

summing_cost.add_cost(path_cost);
}
}
}
_ => {
// For other branching functions, fall back to sequential processing
let mut total_cost = node.cost.min.clone();
for child_cost_node in &node.children {
let child_summing = calculate_total_cost_with_summing(child_cost_node);
let combined_cost = child_summing.add_all();
let _ = total_cost.add(&combined_cost);
}
summing_cost.add_cost(total_cost);
}
}
} else {
// For non-branching, add all costs sequentially
let mut total_cost = node.cost.min.clone();
for child_cost_node in &node.children {
let child_summing = calculate_total_cost_with_summing(child_cost_node);
let combined_cost = child_summing.add_all();
let _ = total_cost.add(&combined_cost);
}
summing_cost.add_cost(total_cost);
}

summing_cost
}

impl From<SummingExecutionCost> for StaticCost {
fn from(summing: SummingExecutionCost) -> Self {
StaticCost {
min: summing.min(),
max: summing.max(),
}
}
}

/// get min & max costs for a given cost function
fn get_costs(
cost_fn: fn(u64) -> Result<ExecutionCost, VmExecutionError>,
arg_count: u64,
) -> Result<ExecutionCost, String> {
let cost = cost_fn(arg_count).map_err(|e| format!("Cost calculation error: {:?}", e))?;
Ok(cost)
}
Loading