diff --git a/Cargo.lock b/Cargo.lock
index bbcbcdf546d..73af20c8a40 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2,6 +2,18 @@
# It is not intended for manual editing.
version = 4
+[[package]]
+name = "TinyUFO"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "011f852ef553046d2f180ecea9e91b9460387d10a75e3a995392e5130d626e20"
+dependencies = [
+ "ahash",
+ "crossbeam-queue",
+ "crossbeam-skiplist",
+ "flurry",
+]
+
[[package]]
name = "addr2line"
version = "0.21.0"
@@ -25,14 +37,16 @@ checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
[[package]]
name = "ahash"
-version = "0.8.8"
+version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "42cd52102d3df161c77a887b608d7a4897d7cc112886a9537b738a887a03aaff"
+checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
dependencies = [
"cfg-if 1.0.0",
+ "const-random",
+ "getrandom 0.3.4",
"once_cell",
"version_check",
- "zerocopy 0.7.32",
+ "zerocopy",
]
[[package]]
@@ -804,6 +818,7 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
name = "clarity"
version = "0.0.1"
dependencies = [
+ "TinyUFO",
"assert-json-diff 1.1.0",
"clarity-types 0.0.1",
"criterion",
@@ -935,6 +950,26 @@ version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
+[[package]]
+name = "const-random"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
+dependencies = [
+ "const-random-macro",
+]
+
+[[package]]
+name = "const-random-macro"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
+dependencies = [
+ "getrandom 0.2.12",
+ "once_cell",
+ "tiny-keccak",
+]
+
[[package]]
name = "core-foundation"
version = "0.9.4"
@@ -1033,6 +1068,25 @@ dependencies = [
"crossbeam-utils",
]
+[[package]]
+name = "crossbeam-queue"
+version = "0.3.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-skiplist"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df29de440c58ca2cc6e587ec3d22347551a32435fbde9d2bff64e78a9ffa151b"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
[[package]]
name = "crossbeam-utils"
version = "0.8.19"
@@ -1452,6 +1506,18 @@ dependencies = [
"libredox 0.1.12",
]
+[[package]]
+name = "flurry"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf5efcf77a4da27927d3ab0509dec5b0954bb3bc59da5a1de9e52642ebd4cdf9"
+dependencies = [
+ "ahash",
+ "num_cpus",
+ "parking_lot",
+ "seize",
+]
+
[[package]]
name = "fnv"
version = "1.0.7"
@@ -1734,7 +1800,7 @@ checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
dependencies = [
"cfg-if 1.0.0",
"crunchy",
- "zerocopy 0.8.27",
+ "zerocopy",
]
[[package]]
@@ -1818,6 +1884,12 @@ version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd5256b483761cd23699d0da46cc6fd2ee3be420bbe6d020ae4a091e70b7e9fd"
+[[package]]
+name = "hermit-abi"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
+
[[package]]
name = "hex"
version = "0.4.3"
@@ -2780,6 +2852,16 @@ dependencies = [
"autocfg",
]
+[[package]]
+name = "num_cpus"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b"
+dependencies = [
+ "hermit-abi 0.5.2",
+ "libc",
+]
+
[[package]]
name = "num_threads"
version = "0.1.6"
@@ -3948,6 +4030,12 @@ dependencies = [
"libc",
]
+[[package]]
+name = "seize"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "689224d06523904ebcc9b482c6a3f4f7fb396096645c4cd10c0d2ff7371a34d3"
+
[[package]]
name = "semver"
version = "1.0.21"
@@ -4873,6 +4961,15 @@ dependencies = [
"time-core",
]
+[[package]]
+name = "tiny-keccak"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
+dependencies = [
+ "crunchy",
+]
+
[[package]]
name = "tiny_http"
version = "0.12.0"
@@ -5778,33 +5875,13 @@ dependencies = [
"rustix 1.1.3",
]
-[[package]]
-name = "zerocopy"
-version = "0.7.32"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be"
-dependencies = [
- "zerocopy-derive 0.7.32",
-]
-
[[package]]
name = "zerocopy"
version = "0.8.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c"
dependencies = [
- "zerocopy-derive 0.8.27",
-]
-
-[[package]]
-name = "zerocopy-derive"
-version = "0.7.32"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.117",
+ "zerocopy-derive",
]
[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index 41800434064..bd2cc188d02 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -37,6 +37,7 @@ rusqlite = { version = "0.31.0", features = ["blob", "serde_json", "i128_blob",
thiserror = "1.0.65"
tikv-jemallocator = "0.5.4"
toml = "0.5.6"
+TinyUFO = { version = "0.8.0", default-features = false }
# Use a bit more than default optimization for
# dev builds to speed up test execution
diff --git a/changelog.d/7049-clarity-type-size-approximation.added b/changelog.d/7049-clarity-type-size-approximation.added
new file mode 100644
index 00000000000..b79a3077027
--- /dev/null
+++ b/changelog.d/7049-clarity-type-size-approximation.added
@@ -0,0 +1 @@
+New `ResidentBytes` trait for types which can approximate their resident memory size (stack+heap)
\ No newline at end of file
diff --git a/changelog.d/7082-clarity-contract-cache b/changelog.d/7082-clarity-contract-cache
new file mode 100644
index 00000000000..af378100609
--- /dev/null
+++ b/changelog.d/7082-clarity-contract-cache
@@ -0,0 +1 @@
+Implements caching of parsed Clarity `Contract`s (deserialized ASTs)
\ No newline at end of file
diff --git a/changelog.d/README.md b/changelog.d/README.md
index d29d8d5c7ad..3e4f4bd8632 100644
--- a/changelog.d/README.md
+++ b/changelog.d/README.md
@@ -24,7 +24,7 @@ CHANGELOG.md.
2. Write the changelog entry text in the file (one or more lines of markdown):
- ```
+ ```text
Added `marf_compress` as a node configuration parameter to enable MARF compression feature ([#6811](https://github.com/stacks-network/stacks-core/pull/6811))
```
diff --git a/clarity-types/src/lib.rs b/clarity-types/src/lib.rs
index 1b1bef2c501..ca9cb1f286c 100644
--- a/clarity-types/src/lib.rs
+++ b/clarity-types/src/lib.rs
@@ -26,6 +26,7 @@ pub use stacks_common::{
pub mod errors;
pub mod representations;
+pub mod resident_bytes;
pub mod types;
pub use errors::{ClarityTypeError, IncomparableError};
diff --git a/clarity-types/src/resident_bytes.rs b/clarity-types/src/resident_bytes.rs
new file mode 100644
index 00000000000..8935bf5459d
--- /dev/null
+++ b/clarity-types/src/resident_bytes.rs
@@ -0,0 +1,1201 @@
+// Copyright (C) 2026 Stacks Open Internet Foundation
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
+use std::mem::size_of;
+use std::sync::Arc;
+
+#[cfg(feature = "developer-mode")]
+use crate::representations::Span;
+use crate::representations::{
+ ClarityName, ContractName, SymbolicExpression, SymbolicExpressionType, TraitDefinition,
+};
+use crate::types::signatures::{
+ BufferLength, CallableSubtype, ListTypeData, SequenceSubtype, StringSubtype, StringUTF8Length,
+ TupleTypeSignature, TypeSignature,
+};
+use crate::types::{
+ ASCIIData, BuffData, CallableData, CharType, FunctionIdentifier, ListData, OptionalData,
+ PrincipalData, QualifiedContractIdentifier, ResponseData, SequenceData, StandardPrincipalData,
+ TraitIdentifier, TupleData, UTF8Data, Value,
+};
+
+/// Estimated overhead for `Arc`: `strong + weak counts + allocation header`.
+const ARC_OVERHEAD: usize = 16;
+
+// The `btree` and `hashmap` modules below contain heuristic constants derived from std's internal
+// implementations (as of Rust 1.94 / hashbrown 0.15). They provide reasonable estimates of
+// structural overhead, not exact byte counts.
+
+/// Layout constants for `std::collections::BTreeMap` / `BTreeSet`.
+///
+/// BTreeMap uses `B=6`, so nodes hold up to `2*B-1 = 11` entries. Leaf nodes store keys+values;
+/// internal nodes add 12 edge pointers. ~32 bytes overhead per node (metadata + allocator header),
+/// ~2/3 average fill (~7 entries/node, ~8 children for internal nodes).
+mod btree {
+ use std::mem::size_of;
+
+ /// Maximum entries per node (`B=6` → `2*B-1 = 11`).
+ pub const NODE_CAPACITY: usize = 11;
+ /// Estimated average entries per node in a steady-state B-tree (~2/3 fill).
+ pub const AVERAGE_FILL: usize = 7;
+ /// Average children per internal node at ~2/3 fill.
+ pub const AVG_FANOUT: usize = AVERAGE_FILL + 1;
+ /// Per-node overhead: `(parent ptr + idx + len + padding) + allocator header`.
+ pub const NODE_OVERHEAD: usize = 32;
+ /// Additional per-node size for internal nodes: `[MaybeUninit>; CAPACITY + 1]`.
+ pub const EDGE_ARRAY_SIZE: usize = (NODE_CAPACITY + 1) * size_of::();
+
+ /// Estimate total BTree node count (leaves + internal) and how many are internal.
+ pub fn node_counts(len: usize) -> (usize, usize) {
+ let leaves = len.div_ceil(AVERAGE_FILL);
+ let mut internal = 0;
+ let mut children_at_level = leaves;
+ while children_at_level > 1 {
+ let parents = children_at_level.div_ceil(AVG_FANOUT);
+ internal += parents;
+ children_at_level = parents;
+ }
+ (leaves + internal, internal)
+ }
+
+ #[cfg(test)]
+ mod tests {
+ use super::*;
+
+ #[test]
+ fn btree_node_counts() {
+ // 7 entries: 1 leaf, 0 internal
+ assert_eq!(node_counts(7), (1, 0));
+ // 12 entries: 2 leaves + 1 internal root
+ let (total, internal) = node_counts(12);
+ assert_eq!(total, 3);
+ assert_eq!(internal, 1);
+ // 0 entries edge case
+ assert_eq!(node_counts(0), (0, 0));
+ }
+ }
+}
+
+/// Layout constants for [`HashMap`] / [`HashSet`] (hashbrown-backed since Rust 1.36).
+///
+/// `hashbrown` uses a 7/8 max load factor and 1-byte control tags per bucket.
+///
+/// The control array is padded by `Group::WIDTH` (4/8/16 depending on SIMD support); we use 16 as
+/// an upper bound.
+mod hashmap {
+ /// Inverse of `hashbrown`'s max load factor (7/8), as a fraction: `buckets ~= (capacity * 8/7)`.
+ pub const LOAD_FACTOR_INV_NUM: usize = 8;
+ pub const LOAD_FACTOR_INV_DEN: usize = 7;
+ /// Upper bound for SIMD group-width padding. In hashbrown 0.15, Group::WIDTH varies by target
+ /// and implementation (4/8/16 bytes), so we use 16 as a conservative upper bound for
+ /// control-byte padding overhead.
+ pub const CONTROL_GROUP_PADDING: usize = 16;
+
+ /// Calculate the number of buckets for a given `HashMap` capacity, based on hashbrown's growth
+ /// strategy and load factor.
+ pub fn buckets_for_capacity(cap: usize) -> usize {
+ (cap * LOAD_FACTOR_INV_NUM).div_ceil(LOAD_FACTOR_INV_DEN)
+ }
+}
+
+/// Approximate in-memory footprint, in bytes.
+///
+/// Split into [`heap_bytes()`](Self::heap_bytes) (children only) and
+/// [`resident_bytes()`](Self::resident_bytes) (inline + heap) to avoid double-counting in nested
+/// types — containers call `heap_bytes()` on children, only the outermost caller should use
+/// `resident_bytes()`.
+pub trait ResidentBytes: Sized {
+ /// Total approximate memory footprint of this instance.
+ ///
+ /// Default implementation: [`size_of::()`](size_of) (inline size) +
+ /// [`heap_bytes()`](Self::heap_bytes) (additional heap allocations).
+ fn resident_bytes(&self) -> usize {
+ // Note: if we ever need to support unsized types, we should switch to size_of_val(self)
+ // here instead of size_of::() and remove the Sized trait bound.
+ std::mem::size_of::() + self.heap_bytes()
+ }
+
+ /// Heap allocations only, beyond the inline size reported by [`size_of::()`](size_of).
+ fn heap_bytes(&self) -> usize;
+}
+
+impl ResidentBytes for String {
+ fn heap_bytes(&self) -> usize {
+ self.capacity()
+ }
+}
+
+impl ResidentBytes for Vec {
+ fn heap_bytes(&self) -> usize {
+ // Backing array: capacity slots (inline size per slot)
+ let backing = self.capacity() * size_of::();
+
+ // Children's heap allocations
+ let children: usize = self.iter().map(|v| v.heap_bytes()).sum();
+
+ // Total heap
+ backing + children
+ }
+}
+
+impl ResidentBytes for Box {
+ fn heap_bytes(&self) -> usize {
+ // Box heap-allocates the pointee: its inline size + its own heap
+ size_of::() + (**self).heap_bytes()
+ }
+}
+
+impl ResidentBytes for Option {
+ fn heap_bytes(&self) -> usize {
+ match self {
+ // For Some, the T is inline in the Option; only count T's heap
+ Some(v) => v.heap_bytes(),
+ None => 0,
+ }
+ }
+}
+
+impl ResidentBytes for Arc {
+ fn heap_bytes(&self) -> usize {
+ // Counts the Arc allocation (header + pointee). Shared backing may be overcounted if
+ // multiple Arc handles to the same allocation are reachable in one measured graph.
+ ARC_OVERHEAD + size_of::() + (**self).heap_bytes()
+ }
+}
+
+impl ResidentBytes for HashMap {
+ fn heap_bytes(&self) -> usize {
+ let cap = self.capacity();
+ if cap == 0 {
+ // HashMap::new() does not allocate until first insert.
+ return 0;
+ }
+
+ let buckets = hashmap::buckets_for_capacity(cap);
+ let backing = buckets * size_of::<(K, V)>() + buckets + hashmap::CONTROL_GROUP_PADDING;
+
+ // Children's heap allocations (only for occupied entries)
+ let children: usize = self
+ .iter()
+ .map(|(k, v)| k.heap_bytes() + v.heap_bytes())
+ .sum();
+
+ backing + children
+ }
+}
+
+impl ResidentBytes for BTreeMap {
+ fn heap_bytes(&self) -> usize {
+ if self.is_empty() {
+ return 0; // Empty BTreeMaps do not allocate on the heap.
+ }
+
+ let (total_nodes, internal_nodes) = btree::node_counts(self.len());
+
+ // Base node size (shared by leaf and internal): overhead + key/value arrays
+ let leaf_size = btree::NODE_OVERHEAD
+ + (btree::NODE_CAPACITY * size_of::())
+ + (btree::NODE_CAPACITY * size_of::());
+ // Internal nodes additionally carry an edge pointer array
+ let structural = total_nodes * leaf_size + internal_nodes * btree::EDGE_ARRAY_SIZE;
+
+ // Children's heap allocations (only for occupied entries)
+ let children: usize = self
+ .iter()
+ .map(|(k, v)| k.heap_bytes() + v.heap_bytes())
+ .sum();
+
+ structural + children
+ }
+}
+
+impl ResidentBytes for BTreeSet {
+ fn heap_bytes(&self) -> usize {
+ if self.is_empty() {
+ return 0;
+ }
+
+ let (total_nodes, internal_nodes) = btree::node_counts(self.len());
+
+ // BTreeSet is backed by BTreeMap — vals array is zero-size
+ let leaf_size = btree::NODE_OVERHEAD + (btree::NODE_CAPACITY * size_of::());
+ let structural = total_nodes * leaf_size + internal_nodes * btree::EDGE_ARRAY_SIZE;
+ let children: usize = self.iter().map(|v| v.heap_bytes()).sum();
+ structural + children
+ }
+}
+
+impl ResidentBytes for HashSet {
+ fn heap_bytes(&self) -> usize {
+ let cap = self.capacity();
+ if cap == 0 {
+ return 0;
+ }
+
+ let buckets = hashmap::buckets_for_capacity(cap);
+ let backing = buckets * size_of::() + buckets + hashmap::CONTROL_GROUP_PADDING;
+ let children: usize = self.iter().map(|v| v.heap_bytes()).sum();
+ backing + children
+ }
+}
+
+impl ResidentBytes for (A, B) {
+ fn heap_bytes(&self) -> usize {
+ self.0.heap_bytes() + self.1.heap_bytes()
+ }
+}
+
+// Primitive types: no heap allocation (stack-only)
+
+impl ResidentBytes for bool {
+ fn heap_bytes(&self) -> usize {
+ 0
+ }
+}
+impl ResidentBytes for u8 {
+ fn heap_bytes(&self) -> usize {
+ 0
+ }
+}
+impl ResidentBytes for u32 {
+ fn heap_bytes(&self) -> usize {
+ 0
+ }
+}
+impl ResidentBytes for u64 {
+ fn heap_bytes(&self) -> usize {
+ 0
+ }
+}
+impl ResidentBytes for u128 {
+ fn heap_bytes(&self) -> usize {
+ 0
+ }
+}
+impl ResidentBytes for i128 {
+ fn heap_bytes(&self) -> usize {
+ 0
+ }
+}
+
+impl ResidentBytes for Value {
+ fn heap_bytes(&self) -> usize {
+ match self {
+ Value::Int(_) | Value::UInt(_) | Value::Bool(_) => 0,
+ Value::Sequence(data) => data.heap_bytes(),
+ Value::Principal(data) => data.heap_bytes(),
+ Value::Tuple(data) => data.heap_bytes(),
+ Value::Optional(data) => data.heap_bytes(),
+ Value::Response(data) => data.heap_bytes(),
+ Value::CallableContract(data) => data.heap_bytes(),
+ }
+ }
+}
+
+impl ResidentBytes for SequenceData {
+ fn heap_bytes(&self) -> usize {
+ match self {
+ SequenceData::Buffer(buf) => buf.heap_bytes(),
+ SequenceData::List(list) => list.heap_bytes(),
+ SequenceData::String(char_type) => char_type.heap_bytes(),
+ }
+ }
+}
+
+impl ResidentBytes for BuffData {
+ fn heap_bytes(&self) -> usize {
+ self.data.heap_bytes()
+ }
+}
+
+impl ResidentBytes for ListData {
+ fn heap_bytes(&self) -> usize {
+ self.data.heap_bytes() + self.type_signature.heap_bytes()
+ }
+}
+
+impl ResidentBytes for CharType {
+ fn heap_bytes(&self) -> usize {
+ match self {
+ CharType::ASCII(data) => data.heap_bytes(),
+ CharType::UTF8(data) => data.heap_bytes(),
+ }
+ }
+}
+
+impl ResidentBytes for ASCIIData {
+ fn heap_bytes(&self) -> usize {
+ self.data.heap_bytes()
+ }
+}
+
+impl ResidentBytes for UTF8Data {
+ fn heap_bytes(&self) -> usize {
+ self.data.heap_bytes()
+ }
+}
+
+impl ResidentBytes for TupleData {
+ fn heap_bytes(&self) -> usize {
+ self.type_signature.heap_bytes() + self.data_map.heap_bytes()
+ }
+}
+
+impl ResidentBytes for OptionalData {
+ fn heap_bytes(&self) -> usize {
+ self.data.heap_bytes()
+ }
+}
+
+impl ResidentBytes for ResponseData {
+ fn heap_bytes(&self) -> usize {
+ self.data.heap_bytes()
+ }
+}
+
+impl ResidentBytes for CallableData {
+ fn heap_bytes(&self) -> usize {
+ self.contract_identifier.heap_bytes() + self.trait_identifier.heap_bytes()
+ }
+}
+
+impl ResidentBytes for PrincipalData {
+ fn heap_bytes(&self) -> usize {
+ match self {
+ PrincipalData::Standard(data) => data.heap_bytes(),
+ PrincipalData::Contract(data) => data.heap_bytes(),
+ }
+ }
+}
+
+impl ResidentBytes for StandardPrincipalData {
+ fn heap_bytes(&self) -> usize {
+ 0 // Fixed-size: u8 + [u8; 20], no heap allocation
+ }
+}
+
+impl ResidentBytes for QualifiedContractIdentifier {
+ fn heap_bytes(&self) -> usize {
+ self.issuer.heap_bytes() + self.name.heap_bytes()
+ }
+}
+
+impl ResidentBytes for ClarityName {
+ fn heap_bytes(&self) -> usize {
+ self.heap_capacity()
+ }
+}
+
+impl ResidentBytes for ContractName {
+ fn heap_bytes(&self) -> usize {
+ self.heap_capacity()
+ }
+}
+
+impl ResidentBytes for TraitIdentifier {
+ fn heap_bytes(&self) -> usize {
+ self.name.heap_bytes() + self.contract_identifier.heap_bytes()
+ }
+}
+
+impl ResidentBytes for FunctionIdentifier {
+ fn heap_bytes(&self) -> usize {
+ self.heap_capacity()
+ }
+}
+
+impl ResidentBytes for TypeSignature {
+ fn heap_bytes(&self) -> usize {
+ match self {
+ TypeSignature::NoType
+ | TypeSignature::IntType
+ | TypeSignature::UIntType
+ | TypeSignature::BoolType
+ | TypeSignature::PrincipalType => 0,
+ TypeSignature::SequenceType(subtype) => subtype.heap_bytes(),
+ TypeSignature::TupleType(tuple_sig) => tuple_sig.heap_bytes(),
+ TypeSignature::OptionalType(inner) => inner.heap_bytes(),
+ TypeSignature::ResponseType(inner) => inner.heap_bytes(),
+ TypeSignature::CallableType(subtype) => subtype.heap_bytes(),
+ TypeSignature::ListUnionType(set) => set.heap_bytes(),
+ TypeSignature::TraitReferenceType(trait_id) => trait_id.heap_bytes(),
+ }
+ }
+}
+
+impl ResidentBytes for TupleTypeSignature {
+ fn heap_bytes(&self) -> usize {
+ // TupleTypeSignature wraps Arc>. get_type_map()
+ // returns &BTreeMap — count Arc overhead + map header + contents.
+ let map_header = size_of::>();
+ ARC_OVERHEAD + map_header + self.get_type_map().heap_bytes()
+ }
+}
+
+impl ResidentBytes for SequenceSubtype {
+ fn heap_bytes(&self) -> usize {
+ match self {
+ SequenceSubtype::BufferType(len) => len.heap_bytes(),
+ SequenceSubtype::ListType(list) => list.heap_bytes(),
+ SequenceSubtype::StringType(string) => string.heap_bytes(),
+ }
+ }
+}
+
+impl ResidentBytes for ListTypeData {
+ fn heap_bytes(&self) -> usize {
+ // max_len: u32 (no heap), entry_type: Box
+ size_of::() + self.get_list_item_type().heap_bytes()
+ }
+}
+
+impl ResidentBytes for StringSubtype {
+ fn heap_bytes(&self) -> usize {
+ 0 // Both variants (ASCII, UTF8) contain only u32 newtypes
+ }
+}
+
+impl ResidentBytes for BufferLength {
+ fn heap_bytes(&self) -> usize {
+ 0 // u32 newtype
+ }
+}
+
+impl ResidentBytes for StringUTF8Length {
+ fn heap_bytes(&self) -> usize {
+ 0 // u32 newtype
+ }
+}
+
+impl ResidentBytes for CallableSubtype {
+ fn heap_bytes(&self) -> usize {
+ match self {
+ CallableSubtype::Principal(id) => id.heap_bytes(),
+ CallableSubtype::Trait(trait_id) => trait_id.heap_bytes(),
+ }
+ }
+}
+
+#[cfg(feature = "developer-mode")]
+impl ResidentBytes for Span {
+ fn heap_bytes(&self) -> usize {
+ 0 // 4 × u32, all inline
+ }
+}
+
+impl ResidentBytes for SymbolicExpression {
+ fn heap_bytes(&self) -> usize {
+ #[allow(unused_mut)]
+ let mut total = self.expr.heap_bytes();
+ // id is u64 — no heap allocation
+
+ #[cfg(feature = "developer-mode")]
+ {
+ // span is inline (no heap), but pre_comments, end_line_comment, and
+ // post_comments have heap allocations via Vec/String.
+ total += self.pre_comments.heap_bytes();
+ total += self.end_line_comment.heap_bytes();
+ total += self.post_comments.heap_bytes();
+ }
+
+ total
+ }
+}
+
+impl ResidentBytes for SymbolicExpressionType {
+ fn heap_bytes(&self) -> usize {
+ match self {
+ SymbolicExpressionType::AtomValue(value)
+ | SymbolicExpressionType::LiteralValue(value) => value.heap_bytes(),
+ SymbolicExpressionType::Atom(name) => name.heap_bytes(),
+ SymbolicExpressionType::List(exprs) => exprs.heap_bytes(),
+ SymbolicExpressionType::Field(trait_id) => trait_id.heap_bytes(),
+ SymbolicExpressionType::TraitReference(name, defn) => {
+ name.heap_bytes() + defn.heap_bytes()
+ }
+ }
+ }
+}
+
+impl ResidentBytes for TraitDefinition {
+ fn heap_bytes(&self) -> usize {
+ match self {
+ TraitDefinition::Defined(id) | TraitDefinition::Imported(id) => id.heap_bytes(),
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ mod primitives {
+ use super::*;
+
+ #[test]
+ fn primitive_heap_bytes_zero() {
+ assert_eq!(true.heap_bytes(), 0);
+ assert_eq!(0u8.heap_bytes(), 0);
+ assert_eq!(0u32.heap_bytes(), 0);
+ assert_eq!(0u64.heap_bytes(), 0);
+ assert_eq!(0u128.heap_bytes(), 0);
+ assert_eq!(0i128.heap_bytes(), 0);
+ }
+
+ #[test]
+ fn u64_resident_bytes() {
+ let v: u64 = 42;
+ assert_eq!(v.resident_bytes(), 8);
+ assert_eq!(v.heap_bytes(), 0);
+ }
+ }
+
+ mod std_containers {
+ use super::*;
+
+ const HASHMAP_CAPACITY_TRANSITIONS: &[(usize, usize, usize)] = &[
+ (0, 0, 0),
+ (1, 3, 4),
+ (4, 7, 8),
+ (8, 14, 16),
+ (15, 28, 32),
+ (29, 56, 64),
+ (57, 112, 128),
+ (113, 224, 256),
+ (225, 448, 512),
+ ];
+
+ #[test]
+ fn string() {
+ let s = String::from("hello world");
+ assert!(s.resident_bytes() >= size_of::() + 11);
+ assert!(s.heap_bytes() >= 11);
+ }
+
+ #[test]
+ fn vec() {
+ let v: Vec = vec![1, 2, 3, 4, 5];
+ assert!(v.heap_bytes() >= 40);
+ assert!(v.resident_bytes() >= size_of::>() + 40);
+ }
+
+ #[test]
+ fn boxed() {
+ let b = Box::new(String::from("boxed"));
+ assert!(b.heap_bytes() >= size_of::() + 5);
+ }
+
+ #[test]
+ fn option_none() {
+ let opt: Option> = None;
+ assert_eq!(opt.heap_bytes(), 0);
+ assert!(opt.resident_bytes() >= size_of::