Skip to content

Commit d929c52

Browse files
Merge pull request stacks-network#6918 from jacinta-stacks/feat/variable-lookup-by-ref
Make lookup_variable return ValueRef
2 parents aef9693 + f380ceb commit d929c52

File tree

73 files changed

+4554
-3238
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

73 files changed

+4554
-3238
lines changed

clarity-types/src/errors/analysis.rs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use crate::diagnostic::{DiagnosableError, Diagnostic};
2121
use crate::errors::{ClarityTypeError, CostErrors};
2222
use crate::execution_cost::ExecutionCost;
2323
use crate::representations::SymbolicExpression;
24-
use crate::types::{TraitIdentifier, TupleTypeSignature, TypeSignature, Value};
24+
use crate::types::{TraitIdentifier, TupleTypeSignature, TypeSignature};
2525

2626
/// What kind of syntax binding was found to be in error?
2727
#[derive(Debug, PartialEq, Clone, Copy)]
@@ -569,17 +569,19 @@ pub enum RuntimeCheckErrorKind {
569569
/// The first `Box<TypeSignature>` wraps the expected type, and the second wraps the actual type.
570570
TypeError(Box<TypeSignature>, Box<TypeSignature>),
571571
/// Value does not match the expected type during type-checking.
572-
/// The `Box<TypeSignature>` wraps the expected type, and the `Box<Value>` wraps the invalid value.
573-
TypeValueError(Box<TypeSignature>, Box<Value>),
572+
/// The `Box<TypeSignature>` wraps the expected type, and the `String` is a
573+
/// truncated display representation of the invalid value.
574+
TypeValueError(Box<TypeSignature>, String),
574575

575576
// Union type mismatch
576577
/// Value does not belong to the expected union of types during type-checking.
577-
/// The `Vec<TypeSignature>` represents the expected types, and the `Box<Value>` wraps the invalid value.
578-
UnionTypeValueError(Vec<TypeSignature>, Box<Value>),
578+
/// The `Vec<TypeSignature>` represents the expected types, and the `String` is a
579+
/// truncated display representation of the invalid value.
580+
UnionTypeValueError(Vec<TypeSignature>, String),
579581

580582
/// Expected a contract principal value but found a different value.
581-
/// The `Box<Value>` wraps the actual value provided.
582-
ExpectedContractPrincipalValue(Box<Value>),
583+
/// The `String` is a truncated display representation of the actual value provided.
584+
ExpectedContractPrincipalValue(String),
583585

584586
// Match type errors
585587
/// Could not determine the type of an expression during analysis.
@@ -789,7 +791,9 @@ impl From<ClarityTypeError> for RuntimeCheckErrorKind {
789791
ClarityTypeError::TypeSignatureTooDeep => Self::TypeSignatureTooDeep,
790792
ClarityTypeError::ValueOutOfBounds => Self::ValueOutOfBounds,
791793
ClarityTypeError::DuplicateTupleField(name) => Self::NameAlreadyUsed(name),
792-
ClarityTypeError::TypeMismatchValue(ty, value) => Self::TypeValueError(ty, value),
794+
ClarityTypeError::TypeMismatchValue(ty, value) => {
795+
Self::TypeValueError(ty, value.to_error_string())
796+
}
793797
ClarityTypeError::TypeMismatch(expected, found) => Self::TypeError(expected, found),
794798
ClarityTypeError::ListTypeMismatch => Self::ListTypesMustMatch,
795799
ClarityTypeError::InvalidAsciiCharacter(_) => Self::InvalidCharactersDetected,

clarity-types/src/types/mod.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ pub const MAX_TO_ASCII_BUFFER_LEN: u32 = (MAX_TO_ASCII_RESULT_LEN - 2) / 2;
5757
pub const MAX_TYPE_DEPTH: u8 = 32;
5858
/// this is the charged size for wrapped values, i.e., response or optionals
5959
pub const WRAPPER_VALUE_SIZE: u32 = 1;
60+
/// Maximum byte length for Value string representations in error messages.
61+
const MAX_ERROR_VALUE_DISPLAY_LEN: usize = 512;
6062

6163
#[derive(Debug, Clone, Eq, Serialize, Deserialize)]
6264
pub struct TupleData {
@@ -1345,6 +1347,21 @@ impl Value {
13451347
))
13461348
}
13471349
}
1350+
1351+
/// Format as a truncated string for use in error messages.
1352+
/// Avoids cloning potentially large Values in error paths.
1353+
pub fn to_error_string(&self) -> String {
1354+
let full = format!("{self:?}");
1355+
if full.len() <= MAX_ERROR_VALUE_DISPLAY_LEN {
1356+
full
1357+
} else {
1358+
let end = (0..=MAX_ERROR_VALUE_DISPLAY_LEN)
1359+
.rev()
1360+
.find(|&i| full.is_char_boundary(i))
1361+
.unwrap_or(0);
1362+
format!("{}...", &full[..end])
1363+
}
1364+
}
13481365
}
13491366

13501367
impl BuffData {

clarity/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ criterion = "0.8.2"
4444
name = "sequence_ops"
4545
harness = false
4646

47+
[[bench]]
48+
name = "value_ref"
49+
harness = false
50+
4751
[target.'cfg(not(target_family = "wasm"))'.dependencies]
4852
serde_stacker = "0.1"
4953

clarity/benches/value_ref.rs

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
// Copyright (C) 2026 Stacks Open Internet Foundation
2+
//
3+
// This program is free software: you can redistribute it and/or modify
4+
// it under the terms of the GNU General Public License as published by
5+
// the Free Software Foundation, either version 3 of the License, or
6+
// (at your option) any later version.
7+
//
8+
// This program is distributed in the hope that it will be useful,
9+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
// GNU General Public License for more details.
12+
//
13+
// You should have received a copy of the GNU General Public License
14+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
15+
16+
//! Benchmarks for the ValueRef zero-copy variable lookup change.
17+
//!
18+
//! Compares Epoch33 (old: `lookup_variable` always clones + sanitizes) against
19+
//! Epoch34 (new: `lookup_variable` returns a borrowed reference for pre-sanitized
20+
//! epochs, deferring or eliminating the clone entirely).
21+
//!
22+
//! Three scenarios exercise the primary beneficiaries:
23+
//!
24+
//! 1. `fold_buf_cmp` — fold over a list where each step does `(>= BIG-BUF BIG-BUF)`.
25+
//! Each step looks up a 128-byte contract constant twice. `special_geq_v2` uses
26+
//! `as_ref()` throughout, so Epoch34 allocates nothing for the operands.
27+
//!
28+
//! 2. `fold_ascii_cmp` — same pattern with a 128-char ASCII string constant.
29+
//!
30+
//! 3. `let_local_refs` — a `let` that binds a 128-byte buffer to `x`, then references
31+
//! `x` N times via `(>= x 0x00)`. Shows the local-variable lookup benefit.
32+
33+
use std::hint::black_box;
34+
35+
use clarity::vm::contexts::{ContractContext, GlobalContext};
36+
use clarity::vm::costs::LimitedCostTracker;
37+
use clarity::vm::database::MemoryBackingStore;
38+
use clarity::vm::representations::SymbolicExpression;
39+
use clarity::vm::types::QualifiedContractIdentifier;
40+
use clarity::vm::version::ClarityVersion;
41+
use clarity::vm::{ast, eval_all};
42+
use criterion::{BatchSize, BenchmarkId, Criterion, criterion_group, criterion_main};
43+
use stacks_common::consts::CHAIN_ID_TESTNET;
44+
use stacks_common::types::StacksEpochId;
45+
46+
const VERSION: ClarityVersion = ClarityVersion::Clarity2;
47+
48+
// ---------------------------------------------------------------------------
49+
// Helpers
50+
// ---------------------------------------------------------------------------
51+
52+
fn parse(source: &str, epoch: StacksEpochId) -> Vec<SymbolicExpression> {
53+
let contract_id = QualifiedContractIdentifier::transient();
54+
let mut cost = LimitedCostTracker::new_free();
55+
ast::build_ast(&contract_id, source, &mut cost, VERSION, epoch)
56+
.expect("failed to parse benchmark program")
57+
.expressions
58+
}
59+
60+
/// Execute `parsed` in a fresh environment for `epoch`.
61+
/// `marf` is provided by the caller (created in `iter_batched` setup) so that
62+
/// SQLite initialisation is excluded from the timing window.
63+
fn run(parsed: &[SymbolicExpression], epoch: StacksEpochId, mut marf: MemoryBackingStore) {
64+
let contract_id = QualifiedContractIdentifier::transient();
65+
let db = marf.as_clarity_db();
66+
let mut global_context = GlobalContext::new(
67+
false,
68+
CHAIN_ID_TESTNET,
69+
db,
70+
LimitedCostTracker::new_free(),
71+
epoch,
72+
);
73+
let mut ctx = ContractContext::new(contract_id, VERSION);
74+
black_box(
75+
global_context
76+
.execute(|g| eval_all(parsed, &mut ctx, g, None))
77+
.unwrap(),
78+
);
79+
}
80+
81+
// ---------------------------------------------------------------------------
82+
// Program generators
83+
// ---------------------------------------------------------------------------
84+
85+
/// `(fold cmp-step (list 1 … steps) true)` where `cmp-step` does
86+
/// `(>= BIG-BUF BIG-BUF)` — 2 contract-constant lookups per step.
87+
fn make_fold_buf_program(steps: usize) -> String {
88+
let buf_hex = "ab".repeat(128); // 128-byte buffer
89+
let list_elems = (1..=steps)
90+
.map(|i| i.to_string())
91+
.collect::<Vec<_>>()
92+
.join(" ");
93+
format!(
94+
r#"
95+
(define-constant BIG-BUF 0x{buf_hex})
96+
(define-private (cmp-step (i int) (acc bool))
97+
(>= BIG-BUF BIG-BUF))
98+
(fold cmp-step (list {list_elems}) true)"#
99+
)
100+
}
101+
102+
/// Same as above but with a 128-char ASCII string constant.
103+
fn make_fold_ascii_program(steps: usize) -> String {
104+
let str_content = "a".repeat(128);
105+
let list_elems = (1..=steps)
106+
.map(|i| i.to_string())
107+
.collect::<Vec<_>>()
108+
.join(" ");
109+
format!(
110+
r#"
111+
(define-constant BIG-STR "{str_content}")
112+
(define-private (cmp-step (i int) (acc bool))
113+
(>= BIG-STR BIG-STR))
114+
(fold cmp-step (list {list_elems}) true)"#
115+
)
116+
}
117+
118+
/// `(let ((x BIG-BUF)) (and (>= x 0x00) … refs times …))`
119+
/// Measures lookup of a local context variable `refs` times.
120+
fn make_let_local_program(refs: usize) -> String {
121+
let buf_hex = "ab".repeat(128);
122+
let comparisons = (0..refs)
123+
.map(|_| "(>= x 0x00)".to_string())
124+
.collect::<Vec<_>>()
125+
.join(" ");
126+
format!(
127+
r#"
128+
(define-constant BIG-BUF 0x{buf_hex})
129+
(let ((x BIG-BUF))
130+
(and {comparisons}))"#
131+
)
132+
}
133+
134+
// ---------------------------------------------------------------------------
135+
// Benchmark groups
136+
// ---------------------------------------------------------------------------
137+
138+
fn bench_fold_buf(c: &mut Criterion) {
139+
let mut group = c.benchmark_group("value_ref/fold_buf_cmp");
140+
for &steps in &[50usize, 200] {
141+
let program = make_fold_buf_program(steps);
142+
let parsed_33 = parse(&program, StacksEpochId::Epoch33);
143+
let parsed_34 = parse(&program, StacksEpochId::Epoch34);
144+
145+
group.bench_function(BenchmarkId::new("epoch33", steps), |b| {
146+
b.iter_batched(
147+
MemoryBackingStore::new,
148+
|marf| run(&parsed_33, StacksEpochId::Epoch33, marf),
149+
BatchSize::SmallInput,
150+
);
151+
});
152+
group.bench_function(BenchmarkId::new("epoch34", steps), |b| {
153+
b.iter_batched(
154+
MemoryBackingStore::new,
155+
|marf| run(&parsed_34, StacksEpochId::Epoch34, marf),
156+
BatchSize::SmallInput,
157+
);
158+
});
159+
}
160+
group.finish();
161+
}
162+
163+
fn bench_fold_ascii(c: &mut Criterion) {
164+
let mut group = c.benchmark_group("value_ref/fold_ascii_cmp");
165+
for &steps in &[50usize, 200] {
166+
let program = make_fold_ascii_program(steps);
167+
let parsed_33 = parse(&program, StacksEpochId::Epoch33);
168+
let parsed_34 = parse(&program, StacksEpochId::Epoch34);
169+
170+
group.bench_function(BenchmarkId::new("epoch33", steps), |b| {
171+
b.iter_batched(
172+
MemoryBackingStore::new,
173+
|marf| run(&parsed_33, StacksEpochId::Epoch33, marf),
174+
BatchSize::SmallInput,
175+
);
176+
});
177+
group.bench_function(BenchmarkId::new("epoch34", steps), |b| {
178+
b.iter_batched(
179+
MemoryBackingStore::new,
180+
|marf| run(&parsed_34, StacksEpochId::Epoch34, marf),
181+
BatchSize::SmallInput,
182+
);
183+
});
184+
}
185+
group.finish();
186+
}
187+
188+
fn bench_let_local(c: &mut Criterion) {
189+
let mut group = c.benchmark_group("value_ref/let_local_refs");
190+
for &refs in &[10usize, 50] {
191+
let program = make_let_local_program(refs);
192+
let parsed_33 = parse(&program, StacksEpochId::Epoch33);
193+
let parsed_34 = parse(&program, StacksEpochId::Epoch34);
194+
195+
group.bench_function(BenchmarkId::new("epoch33", refs), |b| {
196+
b.iter_batched(
197+
MemoryBackingStore::new,
198+
|marf| run(&parsed_33, StacksEpochId::Epoch33, marf),
199+
BatchSize::SmallInput,
200+
);
201+
});
202+
group.bench_function(BenchmarkId::new("epoch34", refs), |b| {
203+
b.iter_batched(
204+
MemoryBackingStore::new,
205+
|marf| run(&parsed_34, StacksEpochId::Epoch34, marf),
206+
BatchSize::SmallInput,
207+
);
208+
});
209+
}
210+
group.finish();
211+
}
212+
213+
criterion_group!(benches, bench_fold_buf, bench_fold_ascii, bench_let_local);
214+
criterion_main!(benches);

0 commit comments

Comments
 (0)