Skip to main content

foundry_evm_fuzz/strategies/
literals.rs

1use alloy_dyn_abi::DynSolType;
2use alloy_primitives::{
3    B256, Bytes, I256, U256, keccak256,
4    map::{B256IndexSet, HashMap, IndexSet},
5};
6use foundry_common::Analysis;
7use foundry_compilers::ProjectPathsConfig;
8use solar::{
9    ast::{
10        self,
11        BinOpKind::{Add, BitAnd, BitOr, BitXor, Div, Mul, Pow, Rem, Shl, Shr, Sub},
12        Visit,
13    },
14    interface::{Span, source_map::FileName},
15};
16use std::{
17    cell::RefCell,
18    ops::ControlFlow,
19    sync::{Arc, OnceLock},
20};
21
22/// Maximum nesting depth [`LiteralsCollector::eval`] recurses into, to bound stack usage.
23const MAX_FOLD_DEPTH: usize = 128;
24
25#[derive(Clone, Debug)]
26pub struct LiteralsDictionary {
27    maps: Arc<OnceLock<LiteralMaps>>,
28}
29
30impl Default for LiteralsDictionary {
31    fn default() -> Self {
32        Self::new(None, None, usize::MAX)
33    }
34}
35
36impl LiteralsDictionary {
37    pub fn new(
38        analysis: Option<Analysis>,
39        paths_config: Option<ProjectPathsConfig>,
40        max_values: usize,
41    ) -> Self {
42        let maps = Arc::new(OnceLock::<LiteralMaps>::new());
43        if let Some(analysis) = analysis
44            && max_values > 0
45        {
46            let maps = maps.clone();
47            // This can't be done in a rayon task (including inside of `get`) because it can cause a
48            // deadlock, since internally `solar` also uses rayon.
49            let _ = std::thread::Builder::new().name("literal-collector".into()).spawn(move || {
50                let _ = maps.get_or_init(|| {
51                    let literals =
52                        LiteralsCollector::process(&analysis, paths_config.as_ref(), max_values);
53                    debug!(
54                        words = literals.words.values().map(|set| set.len()).sum::<usize>(),
55                        strings = literals.strings.len(),
56                        bytes = literals.bytes.len(),
57                        "collected source code literals for fuzz dictionary"
58                    );
59                    literals
60                });
61            });
62        } else {
63            maps.set(Default::default()).unwrap();
64        }
65        Self { maps }
66    }
67
68    /// Returns a reference to the `LiteralMaps`.
69    pub fn get(&self) -> &LiteralMaps {
70        self.maps.wait()
71    }
72
73    /// Test-only helper to seed the dictionary with literal values.
74    #[cfg(test)]
75    pub(crate) fn set(&mut self, map: super::LiteralMaps) {
76        self.maps = Arc::new(OnceLock::new());
77        self.maps.set(map).unwrap();
78    }
79}
80
81#[derive(Debug, Default)]
82pub struct LiteralMaps {
83    pub words: HashMap<DynSolType, B256IndexSet>,
84    pub strings: IndexSet<String>,
85    pub bytes: IndexSet<Bytes>,
86}
87
88/// Maps Solidity enum definitions to their variant counts, used to constrain fuzzed enum inputs
89/// to valid values (the ABI encodes enums as `uint8` without carrying the variant count).
90///
91/// Keys are `"<Contract>.<Enum>"` for enums declared inside a contract/library and the bare
92/// `"<Enum>"` for file-level enums, mirroring the qualifier in an ABI `internalType`.
93#[derive(Clone, Debug, Default)]
94pub struct EnumBounds {
95    inner: Arc<HashMap<String, usize>>,
96}
97
98impl EnumBounds {
99    /// Records the variant count of every enum declaration across all parsed sources (libraries
100    /// and dependencies included, as their enums may be used as test parameters). Keys with
101    /// conflicting counts are dropped, leaving ambiguous enums unbounded rather than bounded wrong.
102    pub fn collect(analysis: &Analysis) -> Self {
103        let bounds = analysis.enter(|compiler| {
104            let mut collector = EnumBoundsCollector::default();
105            for source in compiler.sources().iter() {
106                if let Some(ast) = &source.ast {
107                    let _ = collector.visit_source_unit(ast);
108                }
109            }
110            // Keep only unambiguous entries; ambiguous keys (`None`) are discarded.
111            collector
112                .bounds
113                .into_iter()
114                .filter_map(|(key, count)| count.map(|count| (key, count)))
115                .collect()
116        });
117        Self { inner: Arc::new(bounds) }
118    }
119
120    /// Returns the number of variants for an enum identified by an optional contract qualifier and
121    /// name, or `None` if it is unknown.
122    pub fn variant_count(&self, contract: Option<&str>, name: &str) -> Option<usize> {
123        let key = match contract {
124            Some(contract) => format!("{contract}.{name}"),
125            None => name.to_string(),
126        };
127        self.inner.get(&key).copied()
128    }
129}
130
131/// AST visitor that records enum variant counts, tracking the enclosing contract to build
132/// fully-qualified keys.
133#[derive(Default)]
134struct EnumBoundsCollector {
135    /// Name of the contract/library currently being visited, if any.
136    current_contract: Option<String>,
137    /// Collected `enum key -> variant count` entries. A value of `None` marks a key seen with
138    /// conflicting counts (ambiguous), which is later discarded.
139    bounds: HashMap<String, Option<usize>>,
140}
141
142impl<'ast> ast::Visit<'ast> for EnumBoundsCollector {
143    type BreakValue = ();
144
145    fn visit_item_contract(&mut self, contract: &'ast ast::ItemContract<'ast>) -> ControlFlow<()> {
146        let prev = self.current_contract.replace(contract.name.as_str().to_string());
147        let r = self.walk_item_contract(contract);
148        self.current_contract = prev;
149        r
150    }
151
152    fn visit_item_enum(&mut self, enum_: &'ast ast::ItemEnum<'ast>) -> ControlFlow<()> {
153        let name = enum_.name.as_str();
154        let count = enum_.variants.len();
155        let key = match &self.current_contract {
156            Some(contract) => format!("{contract}.{name}"),
157            None => name.to_string(),
158        };
159        self.bounds
160            .entry(key)
161            // Same key seen with a different count is ambiguous; mark it for removal.
162            .and_modify(|existing| {
163                if *existing != Some(count) {
164                    *existing = None;
165                }
166            })
167            .or_insert(Some(count));
168        self.walk_item_enum(enum_)
169    }
170}
171
172#[derive(Debug, Default)]
173pub struct LiteralsCollector {
174    max_values: usize,
175    total_values: usize,
176    output: LiteralMaps,
177    /// Memoizes [`Self::eval`] results by expression span so overlapping subtrees are folded once.
178    eval_cache: RefCell<HashMap<Span, Option<Num>>>,
179}
180
181impl LiteralsCollector {
182    fn new(max_values: usize) -> Self {
183        Self { max_values, ..Default::default() }
184    }
185
186    pub fn process(
187        analysis: &Analysis,
188        paths_config: Option<&ProjectPathsConfig>,
189        max_values: usize,
190    ) -> LiteralMaps {
191        analysis.enter(|compiler| {
192            let mut literals_collector = Self::new(max_values);
193            for source in compiler.sources().iter() {
194                // Ignore scripts, and libs
195                if let Some(paths) = paths_config
196                    && let FileName::Real(source_path) = &source.file.name
197                    && !(source_path.starts_with(&paths.sources) || paths.is_test(source_path))
198                {
199                    continue;
200                }
201
202                if let Some(ast) = &source.ast
203                    && literals_collector.visit_source_unit(ast).is_break()
204                {
205                    break;
206                }
207            }
208
209            literals_collector.output
210        })
211    }
212
213    /// Inserts a single word value under the given type, respecting the value limit.
214    fn insert_word(&mut self, ty: DynSolType, word: B256) {
215        if self.total_values < self.max_values
216            && self.output.words.entry(ty).or_default().insert(word)
217        {
218            self.total_values += 1;
219        }
220    }
221
222    /// Inserts a string value, respecting the value limit.
223    fn insert_string(&mut self, s: String) {
224        if self.total_values < self.max_values && self.output.strings.insert(s) {
225            self.total_values += 1;
226        }
227    }
228
229    /// Inserts a raw bytes value, respecting the value limit.
230    fn insert_bytes(&mut self, bytes: Bytes) {
231        if self.total_values < self.max_values && self.output.bytes.insert(bytes) {
232            self.total_values += 1;
233        }
234    }
235
236    /// Seeds an unsigned value under all `uintN` sizes that can represent it.
237    fn seed_uint(&mut self, value: U256) {
238        let word = B256::from(value);
239        for bits in [8, 16, 32, 64, 128, 256] {
240            if can_fit_uint(value, bits) {
241                self.insert_word(DynSolType::Uint(bits), word);
242            }
243        }
244        self.insert_word(DynSolType::FixedBytes(32), word);
245    }
246
247    /// Seeds a signed value under all `intN` sizes that can represent it.
248    fn seed_int(&mut self, value: I256) {
249        let word = B256::from(value.into_raw());
250        for bits in [8, 16, 32, 64, 128, 256] {
251            if can_fit_int(value, bits) {
252                self.insert_word(DynSolType::Int(bits), word);
253            }
254        }
255    }
256
257    /// Seeds a folded value: under its exact type when it carries a width, and (for integers) under
258    /// every smaller size that can represent it.
259    fn seed_num(&mut self, value: Num) {
260        match value {
261            Num::Int { raw, signed: false, width } => {
262                if let Some(bits) = width {
263                    self.insert_word(DynSolType::Uint(bits), B256::from(raw));
264                }
265                self.seed_uint(raw);
266            }
267            Num::Int { signed: true, width, .. } => {
268                let i = value.to_i256().expect("signed values always convert to I256");
269                if let Some(bits) = width {
270                    self.insert_word(DynSolType::Int(bits), B256::from(i.into_raw()));
271                }
272                self.seed_int(i);
273            }
274            // `bytesN` is left-aligned in the word, while integers are right-aligned.
275            Num::Bytes { raw, n } => {
276                let word = if n >= 32 { raw } else { raw.wrapping_shl((32 - n) * 8) };
277                self.insert_word(DynSolType::FixedBytes(n), B256::from(word));
278            }
279        }
280    }
281
282    /// Attempts to constant-fold a compound expression and seed the resulting value(s).
283    ///
284    /// This walks recognized casts (`uintN`/`intN`/`bytesN`/`address`), `keccak256` of literal
285    /// arguments, `type(T).min`/`max`, and arithmetic/bitwise expressions over numeric literals.
286    fn fold_and_seed(&mut self, expr: &ast::Expr<'_>) {
287        if let ast::ExprKind::Call(callee, args) = &expr.kind
288            && let Some(arg) = single_arg(args)
289        {
290            match &callee.peel_parens().kind {
291                // A top-level `keccak256(<literal>)` seeds a `bytes32` hash; nested, it is folded
292                // to its numeric value by `eval` instead.
293                ast::ExprKind::Ident(id) if id.as_str() == "keccak256" => {
294                    if let Some(bytes) = lit_bytes(arg) {
295                        self.insert_word(DynSolType::FixedBytes(32), keccak256(bytes));
296                    }
297                    return;
298                }
299                // A top-level `address(...)` cast seeds under `address`; nested, it folds to a
300                // 160-bit integer via `cast_num`.
301                ast::ExprKind::Type(ty)
302                    if matches!(
303                        &ty.kind,
304                        ast::TypeKind::Elementary(ast::ElementaryType::Address(_))
305                    ) =>
306                {
307                    if let Some(value) = self.eval(arg) {
308                        self.insert_word(
309                            DynSolType::Address,
310                            B256::from(low_bits(value.full_raw(), 160)),
311                        );
312                    }
313                    return;
314                }
315                _ => {}
316            }
317        }
318
319        if let Some(value) = self.eval(expr) {
320            self.seed_num(value);
321        }
322    }
323
324    /// Recursively evaluates a constant expression to a numeric value, if possible. Results are
325    /// memoized by span and recursion is depth-bounded.
326    fn eval(&self, expr: &ast::Expr<'_>) -> Option<Num> {
327        self.eval_depth(expr, 0)
328    }
329
330    fn eval_depth(&self, expr: &ast::Expr<'_>, depth: usize) -> Option<Num> {
331        if depth > MAX_FOLD_DEPTH {
332            return None;
333        }
334        let expr = expr.peel_parens();
335        if let Some(cached) = self.eval_cache.borrow().get(&expr.span) {
336            return *cached;
337        }
338        let result = self.eval_kind(expr, depth);
339        // Only memoize successful folds, so a depth-limited `None` doesn't poison a subtree that is
340        // foldable when later visited as a shallower root.
341        if result.is_some() {
342            self.eval_cache.borrow_mut().insert(expr.span, result);
343        }
344        result
345    }
346
347    fn eval_kind(&self, expr: &ast::Expr<'_>, depth: usize) -> Option<Num> {
348        match &expr.kind {
349            ast::ExprKind::Lit(lit, _) => match &lit.kind {
350                // Sub-denominations (e.g. `ether`, `days`) are already folded into the value.
351                ast::LitKind::Number(n) => Some(Num::untyped(U256::from(*n))),
352                _ => None,
353            },
354            ast::ExprKind::Unary(op, inner) => {
355                let value = self.eval_depth(inner, depth + 1)?;
356                match op.kind {
357                    ast::UnOpKind::Neg => value.neg(),
358                    // Bitwise-not complements the raw bits, preserving signedness and width.
359                    ast::UnOpKind::BitNot => match value {
360                        Num::Int { raw, signed, width } => Some(Num::int(!raw, signed, width)),
361                        Num::Bytes { .. } => None,
362                    },
363                    _ => None,
364                }
365            }
366            ast::ExprKind::Binary(lhs, op, rhs) => {
367                let a = self.eval_depth(lhs, depth + 1)?;
368                let b = self.eval_depth(rhs, depth + 1)?;
369                apply_bin(op.kind, a, b)
370            }
371            ast::ExprKind::Call(callee, args) => {
372                let arg = single_arg(args)?;
373                match &callee.peel_parens().kind {
374                    ast::ExprKind::Type(ty) => {
375                        let ast::TypeKind::Elementary(et) = &ty.kind else { return None };
376                        let value = self.eval_depth(arg, depth + 1)?;
377                        cast_num(*et, value)
378                    }
379                    ast::ExprKind::Ident(id) if id.as_str() == "keccak256" => {
380                        let bytes = lit_bytes(arg)?;
381                        Some(Num::untyped(U256::from_be_bytes(keccak256(bytes).0)))
382                    }
383                    _ => None,
384                }
385            }
386            // `type(uintN).max`, `type(intN).min`, `type(intN).max`.
387            ast::ExprKind::Member(inner, member) => {
388                let ast::ExprKind::TypeCall(ty) = &inner.peel_parens().kind else { return None };
389                let ast::TypeKind::Elementary(et) = &ty.kind else { return None };
390                type_min_max(*et, member.as_str())
391            }
392            _ => None,
393        }
394    }
395}
396
397impl<'ast> ast::Visit<'ast> for LiteralsCollector {
398    type BreakValue = ();
399
400    fn visit_expr(&mut self, expr: &'ast ast::Expr<'ast>) -> ControlFlow<()> {
401        // Stop early if we've hit the limit
402        if self.total_values >= self.max_values {
403            return ControlFlow::Break(());
404        }
405
406        match &expr.kind {
407            // Handle plain literals.
408            ast::ExprKind::Lit(lit, _) => match &lit.kind {
409                ast::LitKind::Number(n) => self.seed_uint(U256::from(*n)),
410                ast::LitKind::Address(addr) => {
411                    self.insert_word(DynSolType::Address, addr.into_word())
412                }
413                ast::LitKind::Str(ast::StrKind::Hex, sym, _) => {
414                    self.insert_bytes(Bytes::copy_from_slice(sym.as_byte_str()));
415                }
416                ast::LitKind::Str(_, sym, _) => {
417                    let s = String::from_utf8_lossy(sym.as_byte_str()).into_owned();
418                    // For strings, also store the hashed version.
419                    self.insert_word(DynSolType::FixedBytes(32), keccak256(s.as_bytes()));
420                    // And the right-padded version if it fits.
421                    if s.len() <= 32 {
422                        self.insert_word(
423                            DynSolType::FixedBytes(32),
424                            B256::right_padding_from(s.as_bytes()),
425                        );
426                    }
427                    self.insert_string(s);
428                }
429                ast::LitKind::Bool(..) | ast::LitKind::Rational(..) | ast::LitKind::Err(..) => {
430                    // ignore
431                }
432            },
433            // Attempt to constant-fold compound expressions (casts, hashes, arithmetic).
434            _ => self.fold_and_seed(expr),
435        }
436
437        self.walk_expr(expr)
438    }
439}
440
441/// A folded constant value. Opportunistic 256-bit heuristic, not a Solidity-correct folder.
442#[derive(Clone, Copy, Debug)]
443enum Num {
444    /// An integer: low-`width` two's-complement bits (right-aligned), signedness, and the bit width
445    /// it was cast to (`None` = untyped literal, treated as 256-bit).
446    Int { raw: U256, signed: bool, width: Option<usize> },
447    /// A `bytesN` value: the `n` significant bytes stored right-aligned.
448    Bytes { raw: U256, n: usize },
449}
450
451impl Num {
452    /// Creates an untyped (256-bit) unsigned integer from a literal.
453    const fn untyped(raw: U256) -> Self {
454        Self::Int { raw, signed: false, width: None }
455    }
456
457    /// Creates an integer, normalizing `raw` to its width's low bits.
458    fn int(raw: U256, signed: bool, width: Option<usize>) -> Self {
459        let raw = match width {
460            Some(bits) => low_bits(raw, bits),
461            None => raw,
462        };
463        Self::Int { raw, signed, width }
464    }
465
466    /// Returns the in-width raw bits of the value (right-aligned, not sign-extended).
467    const fn as_u256(self) -> U256 {
468        match self {
469            Self::Int { raw, .. } | Self::Bytes { raw, .. } => raw,
470        }
471    }
472
473    /// Returns the full 256-bit value, sign-extended from its width in a signed context. Used when
474    /// widening through a cast so the sign is preserved.
475    fn full_raw(self) -> U256 {
476        match self {
477            Self::Int { raw, signed: true, width } => sign_extend(raw, width),
478            _ => self.as_u256(),
479        }
480    }
481
482    /// Returns the value as an [`I256`], sign-extended from its width, if it represents a signed
483    /// integer (in a signed context, or an unsigned value that fits the positive range).
484    fn to_i256(self) -> Option<I256> {
485        match self {
486            Self::Int { raw, signed: true, width } => Some(I256::from_raw(sign_extend(raw, width))),
487            Self::Int { raw, signed: false, .. } => I256::try_from(raw).ok(),
488            Self::Bytes { .. } => None,
489        }
490    }
491
492    /// Returns `true` if the value is in a signed context.
493    const fn is_signed(self) -> bool {
494        matches!(self, Self::Int { signed: true, .. })
495    }
496
497    /// Returns the bit width carried by the value, if any.
498    const fn width(self) -> Option<usize> {
499        match self {
500            Self::Int { width, .. } => width,
501            Self::Bytes { .. } => None,
502        }
503    }
504
505    /// Negates the value, keeping its width and switching to a signed context. Returns `None` if
506    /// the result doesn't fit [`I256`] or, for a width-carrying value, its `intN` type (negating
507    /// `type(intN).min` reverts under checked arithmetic).
508    fn neg(self) -> Option<Self> {
509        match self {
510            // `-x` fits in `I256` iff `x <= 2**255` (`|I256::MIN|`).
511            Self::Int { raw, signed: false, width } => {
512                (raw <= I256::MIN.into_raw()).then(|| Self::int(raw.wrapping_neg(), true, width))
513            }
514            Self::Int { signed: true, width, .. } => {
515                let r = self.to_i256()?.checked_neg()?;
516                if let Some(bits) = width
517                    && !can_fit_int(r, bits)
518                {
519                    return None;
520                }
521                Some(Self::int(r.into_raw(), true, width))
522            }
523            Self::Bytes { .. } => None,
524        }
525    }
526}
527
528/// Applies a binary operator to two folded numeric values, carrying the result width so narrow
529/// operands produce in-range results.
530fn apply_bin(op: ast::BinOpKind, a: Num, b: Num) -> Option<Num> {
531    // `bytesN` operands aren't folded as integers.
532    if matches!(a, Num::Bytes { .. }) || matches!(b, Num::Bytes { .. }) {
533        return None;
534    }
535
536    let signed = a.is_signed() || b.is_signed();
537    // The result type of a shift is the left operand's type, and of `**` the base's; other
538    // operators take the wider operand.
539    let width =
540        if matches!(op, Shl | Shr | Pow) { a.width() } else { combine_width(a.width(), b.width()) };
541
542    // Bitwise AND/OR/XOR and left-shift operate purely on the (in-width) raw bits and are
543    // independent of signedness, so they fold in any context.
544    if matches!(op, BitAnd | BitOr | BitXor | Shl) {
545        let (x, y) = (a.as_u256(), b.as_u256());
546        let raw = match op {
547            BitAnd => x & y,
548            BitOr => x | y,
549            BitXor => x ^ y,
550            Shl => shift_amount(y).map_or(U256::ZERO, |s| x.wrapping_shl(s)),
551            _ => unreachable!(),
552        };
553        return Some(Num::int(raw, signed, width));
554    }
555
556    // Use signed arithmetic if either operand is in a signed context. Signed `>>` (arithmetic) is
557    // not folded here.
558    if signed {
559        let (x, y) = (a.to_i256()?, b.to_i256()?);
560        if op == Pow {
561            return signed_pow(x, y, width);
562        }
563        // Mirror the unsigned path: typed signed operands use checked arithmetic (an overflow of
564        // their `intN` type reverts at runtime), untyped literals keep 256-bit wrapping.
565        let r = match width {
566            Some(bits) => {
567                let r = match op {
568                    Add => x.checked_add(y)?,
569                    Sub => x.checked_sub(y)?,
570                    Mul => x.checked_mul(y)?,
571                    Div => x.checked_div(y)?,
572                    Rem => x.checked_rem(y)?,
573                    _ => return None,
574                };
575                if !can_fit_int(r, bits) {
576                    return None;
577                }
578                r
579            }
580            None => match op {
581                Add => x.wrapping_add(y),
582                Sub => x.wrapping_sub(y),
583                Mul => x.wrapping_mul(y),
584                Div => x.checked_div(y)?,
585                Rem => x.checked_rem(y)?,
586                _ => return None,
587            },
588        };
589        return Some(Num::int(r.into_raw(), true, width));
590    }
591
592    let (x, y) = (a.as_u256(), b.as_u256());
593    let r = match op {
594        // Default-checked arithmetic: a width-carrying op overflowing its type reverts at runtime,
595        // so bail (`None`) instead of seeding the wrapped value. Untyped literals keep 256-bit
596        // wrapping.
597        Add => checked_arith(x.checked_add(y), x.wrapping_add(y), width)?,
598        Sub => checked_arith(x.checked_sub(y), x.wrapping_sub(y), width)?,
599        Mul => checked_arith(x.checked_mul(y), x.wrapping_mul(y), width)?,
600        Div => x.checked_div(y)?,
601        Rem => x.checked_rem(y)?,
602        // `pow` overflowing `U256` means an out-of-range constant, so bail rather than seed a
603        // wrapped value. (`2 ** 256 - 1` therefore doesn't fold; use `type(uint256).max`.) A
604        // width-carrying result must also fit its type, else checked arithmetic would revert.
605        Pow => {
606            let r = x.checked_pow(y)?;
607            checked_arith(Some(r), r, width)?
608        }
609        // Unsigned `>>` is a logical shift.
610        Shr => shift_amount(y).map_or(U256::ZERO, |s| x.wrapping_shr(s)),
611        // Comparisons and logical operators are not folded into values.
612        _ => return None,
613    };
614    Some(Num::int(r, false, width))
615}
616
617/// Resolves a default-checked unsigned arithmetic result.
618///
619/// For a width-carrying operand the result type is checked: it must neither overflow 256 bits
620/// (`checked` is `None`) nor exceed its `uintN` width, otherwise the expression reverts at runtime
621/// and we return `None` instead of seeding an unreachable value. Untyped literal operands keep the
622/// 256-bit wrapping value.
623fn checked_arith(checked: Option<U256>, wrapping: U256, width: Option<usize>) -> Option<U256> {
624    match width {
625        Some(bits) => checked.filter(|r| can_fit_uint(*r, bits)),
626        None => Some(wrapping),
627    }
628}
629
630/// Folds a signed `base ** exp`, returning `None` on a negative exponent or a magnitude that
631/// doesn't fit the signed range (overflow ~ compile error).
632fn signed_pow(base: I256, exp: I256, width: Option<usize>) -> Option<Num> {
633    if exp.is_negative() {
634        return None;
635    }
636    let magnitude = base.unsigned_abs().checked_pow(exp.into_raw())?;
637    let negative = base.is_negative() && exp.into_raw().bit(0);
638    let value = if negative {
639        // Negative results must fit `[-2**255, 0)`, i.e. magnitude <= |I256::MIN|.
640        (magnitude <= I256::MIN.into_raw()).then(|| I256::from_raw(magnitude.wrapping_neg()))?
641    } else {
642        // Positive results must fit `[0, 2**255)`.
643        (magnitude < I256::MIN.into_raw()).then(|| I256::from_raw(magnitude))?
644    };
645    // A width-carrying result that overflows its `intN` type reverts under checked arithmetic, so
646    // don't seed a value the expression can never produce.
647    if let Some(bits) = width
648        && !can_fit_int(value, bits)
649    {
650        return None;
651    }
652    Some(Num::int(value.into_raw(), true, width))
653}
654
655/// Combines the widths of two operands: an untyped operand (`None`) inherits the other's width;
656/// two explicit widths pick the wider.
657fn combine_width(a: Option<usize>, b: Option<usize>) -> Option<usize> {
658    match (a, b) {
659        (None, None) => None,
660        (Some(a), Some(b)) => Some(a.max(b)),
661        (Some(w), None) | (None, Some(w)) => Some(w),
662    }
663}
664
665/// Folds `type(uintN).max`, `type(intN).min`, and `type(intN).max` (and `type(uintN).min` == 0).
666fn type_min_max(ty: ast::ElementaryType, member: &str) -> Option<Num> {
667    match (ty, member) {
668        (ast::ElementaryType::UInt(size), "max") => {
669            let bits = size.bits() as usize;
670            Some(Num::int(low_bits(U256::MAX, bits), false, Some(bits)))
671        }
672        (ast::ElementaryType::UInt(size), "min") => {
673            Some(Num::int(U256::ZERO, false, Some(size.bits() as usize)))
674        }
675        // `intN` max is `2**(N-1) - 1`; min is `-2**(N-1)`, whose two's-complement is just the
676        // sign bit set within the width.
677        (ast::ElementaryType::Int(size), "max") => {
678            let bits = size.bits() as usize;
679            Some(Num::int(low_bits(U256::MAX, bits - 1), true, Some(bits)))
680        }
681        (ast::ElementaryType::Int(size), "min") => {
682            let bits = size.bits() as usize;
683            Some(Num::int(U256::from(1).wrapping_shl(bits - 1), true, Some(bits)))
684        }
685        _ => None,
686    }
687}
688
689/// Returns the shift amount as a `usize`, or `None` if it is `>= 256` (which shifts out all bits).
690fn shift_amount(y: U256) -> Option<usize> {
691    (y < U256::from(256u64)).then(|| y.as_limbs()[0] as usize)
692}
693
694/// Reinterprets a folded value for an elementary type cast used inside a larger expression,
695/// applying the target width so truncation and sign-extension are correct (e.g.
696/// `int256(uint256(type(uint256).max))` -> `-1`).
697fn cast_num(ty: ast::ElementaryType, value: Num) -> Option<Num> {
698    match ty {
699        ast::ElementaryType::UInt(size) => {
700            Some(Num::int(value.full_raw(), false, Some(size.bits() as usize)))
701        }
702        ast::ElementaryType::Int(size) => {
703            Some(Num::int(value.full_raw(), true, Some(size.bits() as usize)))
704        }
705        ast::ElementaryType::Address(_) => Some(Num::int(value.full_raw(), false, Some(160))),
706        ast::ElementaryType::FixedBytes(size) => Some(cast_to_bytes(value, size.bytes() as usize)),
707        _ => None,
708    }
709}
710
711/// Casts a folded value to `bytesN`, keeping the value right-aligned in `Num::Bytes`.
712///
713/// `bytesM(bytesN)` keeps the leftmost `min(M, N)` bytes (and pads on the right when widening);
714/// casting an integer keeps its low `N` bytes.
715fn cast_to_bytes(value: Num, n: usize) -> Num {
716    let raw = match value {
717        Num::Bytes { raw, n: m } if n <= m => raw.wrapping_shr((m - n) * 8),
718        Num::Bytes { raw, n: m } => raw.wrapping_shl((n - m) * 8),
719        _ => low_bits(value.full_raw(), n * 8),
720    };
721    Num::Bytes { raw, n }
722}
723
724/// Returns the low `bits` of `value`, zeroing everything above.
725fn low_bits(value: U256, bits: usize) -> U256 {
726    if bits >= 256 { value } else { value & (U256::from(1).wrapping_shl(bits) - U256::from(1)) }
727}
728
729/// Sign-extends the low `width` bits of `raw` to a full 256-bit two's-complement value.
730fn sign_extend(raw: U256, width: Option<usize>) -> U256 {
731    match width {
732        Some(bits) if bits < 256 && raw.bit(bits - 1) => raw | U256::MAX.wrapping_shl(bits),
733        _ => raw,
734    }
735}
736
737/// Returns the single positional argument of a call, if it has exactly one.
738fn single_arg<'a, 'ast>(args: &'a ast::CallArgs<'ast>) -> Option<&'a ast::Expr<'ast>> {
739    let mut exprs = args.exprs();
740    (exprs.len() == 1).then(|| exprs.next()).flatten()
741}
742
743/// Extracts the raw bytes of a string, unicode, or hex string literal argument, borrowing them
744/// directly from the interner to avoid allocating (and re-hashing) on every fold.
745fn lit_bytes<'a>(expr: &'a ast::Expr<'_>) -> Option<&'a [u8]> {
746    if let ast::ExprKind::Lit(lit, _) = &expr.peel_parens().kind
747        && let ast::LitKind::Str(_, sym, _) = &lit.kind
748    {
749        return Some(sym.as_byte_str());
750    }
751    None
752}
753
754/// Checks if a signed integer value can fit in intN type.
755fn can_fit_int(value: I256, bits: usize) -> bool {
756    // Calculate the maximum positive value for intN: 2^(N-1) - 1
757    let max_val = I256::try_from((U256::from(1) << (bits - 1)) - U256::from(1))
758        .expect("max value should fit in I256");
759    // Calculate the minimum negative value for intN: -2^(N-1)
760    let min_val = -max_val - I256::ONE;
761
762    value >= min_val && value <= max_val
763}
764
765/// Checks if an unsigned integer value can fit in uintN type.
766fn can_fit_uint(value: U256, bits: usize) -> bool {
767    if bits == 256 {
768        return true;
769    }
770    // Calculate the maximum value for uintN: 2^N - 1
771    let max_val = (U256::from(1) << bits) - U256::from(1);
772    value <= max_val
773}
774
775#[cfg(test)]
776mod tests {
777    use super::*;
778    use alloy_primitives::address;
779    use solar::interface::{Session, source_map};
780
781    const SOURCE: &str = r#"
782    contract Magic {
783        // plain literals
784        address constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
785        uint64 constant MAGIC_NUMBER = 1122334455;
786        int32 constant MAGIC_INT = -777;
787        bytes32 constant MAGIC_WORD = "abcd1234";
788        bytes constant MAGIC_BYTES = hex"deadbeef";
789        string constant MAGIC_STRING = "xyzzy";
790
791        // constant exprs with folding
792        uint256 constant NEG_FOLDING = uint(-2);
793        uint256 constant BIN_FOLDING = 2 * 2 ether;
794        bytes32 constant IMPLEMENTATION_SLOT = bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1);
795    }"#;
796
797    #[test]
798    fn test_literals_collector_coverage() {
799        let map = process_source_literals(SOURCE);
800
801        // Expected values from the SOURCE contract
802        let addr = address!("0x6B175474E89094C44Da98b954EedeAC495271d0F").into_word();
803        let num = B256::from(U256::from(1122334455u64));
804        let int = B256::from(I256::try_from(-777i32).unwrap().into_raw());
805        let word = B256::right_padding_from(b"abcd1234");
806        let dyn_bytes = Bytes::from_static(&[0xde, 0xad, 0xbe, 0xef]);
807
808        assert_word(&map, DynSolType::Address, addr, "Expected DAI in address set");
809        assert_word(&map, DynSolType::Uint(64), num, "Expected MAGIC_NUMBER in uint64 set");
810        assert_word(&map, DynSolType::FixedBytes(32), num, "Expected MAGIC_NUMBER in bytes32 set");
811        assert_word(&map, DynSolType::Int(32), int, "Expected MAGIC_INT in int32 set");
812        assert_word(&map, DynSolType::FixedBytes(32), word, "Expected MAGIC_WORD in bytes32 set");
813        assert!(map.strings.contains("xyzzy"), "Expected MAGIC_STRING to be collected");
814        assert!(
815            map.strings.contains("eip1967.proxy.implementation"),
816            "Expected IMPLEMENTATION_SLOT in string set"
817        );
818        assert!(map.bytes.contains(&dyn_bytes), "Expected MAGIC_BYTES in bytes set");
819
820        // -- folded constant expressions --
821
822        // `uint(-2)` folds to `2**256 - 2`.
823        let neg_cast = B256::from(U256::MAX - U256::from(1));
824        assert_word(&map, DynSolType::Uint(256), neg_cast, "Expected uint(-2) to be folded");
825
826        // `2 * 2 ether` folds to `4e18`.
827        let bin = B256::from(U256::from(4_000_000_000_000_000_000u64));
828        assert_word(&map, DynSolType::Uint(64), bin, "Expected `2 * 2 ether` to be folded");
829
830        // `bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)` folds to the
831        // well-known EIP-1967 implementation slot.
832        let slot = B256::from(
833            U256::from_be_bytes(keccak256("eip1967.proxy.implementation").0) - U256::from(1),
834        );
835        assert_word(
836            &map,
837            DynSolType::FixedBytes(32),
838            slot,
839            "Expected IMPLEMENTATION_SLOT expression to be folded",
840        );
841    }
842
843    #[test]
844    fn test_literals_collector_size() {
845        let literals = process_source_literals(SOURCE);
846
847        // Helper to get count for a type, returns 0 if not present
848        let count = |ty: DynSolType| literals.words.get(&ty).map_or(0, |set| set.len());
849
850        assert_eq!(count(DynSolType::Address), 1, "Address literal count mismatch");
851        assert_eq!(literals.strings.len(), 3, "String literals count mismatch");
852        assert_eq!(literals.bytes.len(), 1, "Byte literals count mismatch");
853
854        // Unsigned integers. Bare literals {1, 2, 777, 1122334455, 2e18} are seeded under every
855        // `uintN` that fits, plus the folded values `4e18` (`2 * 2 ether`) and, for `uint256`,
856        // `2**256 - 2` (`uint(-2)`), `K` and `K - 1` where `K = keccak256("eip1967...")`.
857        assert_eq!(count(DynSolType::Uint(8)), 2, "Uint(8) count mismatch");
858        assert_eq!(count(DynSolType::Uint(16)), 3, "Uint(16) count mismatch");
859        assert_eq!(count(DynSolType::Uint(32)), 4, "Uint(32) count mismatch");
860        assert_eq!(count(DynSolType::Uint(64)), 6, "Uint(64) count mismatch");
861        assert_eq!(count(DynSolType::Uint(128)), 6, "Uint(128) count mismatch");
862        assert_eq!(count(DynSolType::Uint(256)), 9, "Uint(256) count mismatch");
863
864        // Signed integers - MAGIC_INT (-777) and the folded `-2` appear in multiple sizes; only
865        // `-2` fits `int8` (-777 is out of range).
866        assert_eq!(count(DynSolType::Int(8)), 1, "Int(8) count mismatch");
867        assert_eq!(count(DynSolType::Int(16)), 2, "Int(16) count mismatch");
868        assert_eq!(count(DynSolType::Int(32)), 2, "Int(32) count mismatch");
869        assert_eq!(count(DynSolType::Int(64)), 2, "Int(64) count mismatch");
870        assert_eq!(count(DynSolType::Int(128)), 2, "Int(128) count mismatch");
871        assert_eq!(count(DynSolType::Int(256)), 2, "Int(256) count mismatch");
872
873        // FixedBytes(32) includes:
874        // - MAGIC_WORD
875        // - String literals (hashed and right-padded versions)
876        // - The folded EIP-1967 slot `K - 1` (`K` itself dedups with the hashed string literal)
877        // - Unsigned numeric values, which use the same ABI word representation
878        assert_eq!(count(DynSolType::FixedBytes(32)), 14, "FixedBytes(32) count mismatch");
879
880        // Total count check
881        assert_eq!(
882            literals.words.values().map(|set| set.len()).sum::<usize>(),
883            56,
884            "Total word values count mismatch"
885        );
886    }
887
888    #[test]
889    fn test_width_aware_casts() {
890        // Casts truncate/sign-extend to the target width, widening respects the source's
891        // signedness, and `~`/signed `**` keep the signed context.
892        let source = r#"
893        contract C {
894            uint8 constant A = uint8(-2);                   // 254
895            uint8 constant B = uint8(257);                  // 1
896            int8 constant D = int8(255);                    // -1
897            int256 constant E = int256(1) - 2;              // -1
898            int256 constant F = ~int256(0);                 // -1 (not 2**256 - 1)
899            int16 constant G = int16(int8(-1));             // -1 (sign-extended)
900            int16 constant H = int16(uint8(255));           // 255 (unsigned source)
901            uint16 constant I = uint16(int8(-1));           // 65535
902            int256 constant J = -2 ** 255;                  // int256 min
903            bytes4 constant K = bytes4(uint32(0x12345678)); // left-aligned
904        }"#;
905        let map = process_source_literals(source);
906
907        let neg_one = B256::from(I256::try_from(-1).unwrap().into_raw());
908        assert_word(&map, DynSolType::Uint(8), B256::from(U256::from(254)), "uint8(-2) -> 254");
909        assert_word(&map, DynSolType::Uint(8), B256::from(U256::from(1)), "uint8(257) -> 1");
910        assert_word(&map, DynSolType::Int(8), neg_one, "int8(255) -> -1");
911        assert_word(&map, DynSolType::Int(256), neg_one, "int256(1) - 2 -> -1");
912        assert_word(&map, DynSolType::Int(256), neg_one, "~int256(0) -> -1");
913        assert_word(&map, DynSolType::Int(16), neg_one, "int16(int8(-1)) -> -1");
914        assert_word(
915            &map,
916            DynSolType::Int(16),
917            B256::from(U256::from(255)),
918            "int16(uint8(255)) -> 255",
919        );
920        assert_word(
921            &map,
922            DynSolType::Uint(16),
923            B256::from(U256::from(65535)),
924            "uint16(int8(-1)) -> 65535",
925        );
926        assert_word(
927            &map,
928            DynSolType::Int(256),
929            B256::from(I256::MIN.into_raw()),
930            "-2 ** 255 -> int256 min",
931        );
932
933        let left_aligned = B256::right_padding_from(&[0x12, 0x34, 0x56, 0x78]);
934        assert_word(&map, DynSolType::FixedBytes(4), left_aligned, "bytes4 is left-aligned");
935    }
936
937    #[test]
938    fn test_width_dependent_ops_stay_in_width() {
939        // `~`, shifts, and arithmetic on a narrow operand stay within its width instead of leaking
940        // 256-bit results; an untyped literal inherits the typed operand's width.
941        let source = r#"
942        contract C {
943            uint8 constant A = ~uint8(0);                 // 255
944            uint8 constant B = uint8(1) << 8;             // 0
945            int8 constant D = int8(1) << 7;               // -128
946            uint8 constant E = uint8(255) << 256;         // 0 (shift amount >= 256)
947            uint8 constant F = uint8(250) + 5;            // 255 (in-range, typed + untyped literal)
948            uint8 constant G = uint8(10) ** uint256(2);   // 100 (in-range, result type is base uint8)
949            uint8 constant H = uint8(0x80) >> uint256(0); // 128 (result type is left uint8)
950        }"#;
951        let map = process_source_literals(source);
952
953        assert_word(&map, DynSolType::Uint(8), B256::from(U256::from(255)), "~uint8(0) -> 255");
954        assert_word(&map, DynSolType::Uint(8), B256::from(U256::ZERO), "uint8(_) << {8,256} -> 0");
955        assert_word(
956            &map,
957            DynSolType::Uint(8),
958            B256::from(U256::from(255)),
959            "uint8(250) + 5 -> 255",
960        );
961        assert_word(
962            &map,
963            DynSolType::Uint(8),
964            B256::from(U256::from(100)),
965            "uint8(10) ** 2 -> 100",
966        );
967        assert_word(
968            &map,
969            DynSolType::Uint(8),
970            B256::from(U256::from(128)),
971            "uint8(0x80) >> 0 -> 128",
972        );
973        let neg_128 = B256::from(I256::try_from(-128).unwrap().into_raw());
974        assert_word(&map, DynSolType::Int(8), neg_128, "int8(1) << 7 -> -128");
975
976        // The in-width `~` result must not leak its 256-bit form (`uint256::MAX`) into a larger
977        // bucket. (In-range arithmetic results like `255` legitimately seed every width that fits.)
978        assert!(
979            !map.words
980                .get(&DynSolType::Uint(256))
981                .is_some_and(|s| s.contains(&B256::from(U256::MAX))),
982            "~uint8(0) must not seed a uint256 max"
983        );
984    }
985
986    #[test]
987    fn test_checked_overflow_does_not_seed() {
988        // Solidity arithmetic is checked by default: a width-carrying `+`/`-`/`*`/`**` that
989        // overflows its type reverts at runtime, so the folder must not seed the (wrapped) value
990        // that can never occur. Operands and in-range siblings still fold normally.
991        let source = r#"
992        contract C {
993            uint8 constant A = uint8(250) + 10;   // 260 -> reverts (panic 0x11), not 4
994            uint8 constant B = uint8(200) * 2;    // 400 -> reverts, not 144
995            uint8 constant C2 = uint8(1) - 2;     // underflow -> reverts, not 255
996            uint8 constant D = uint8(10) ** 3;    // 1000 -> reverts, not 232
997            int8 constant E = int8(100) + 100;    // 200 -> reverts, not -56
998            int8 constant F = int8(64) * 2;       // 128 -> reverts, not -128
999            int8 constant G = int8(5) ** 3;       // 125 -> in range, folds
1000        }"#;
1001        let map = process_source_literals(source);
1002
1003        // None of the wrapped values may be seeded under any width.
1004        let seeded =
1005            |ty, raw: U256| map.words.get(&ty).is_some_and(|s| s.contains(&B256::from(raw)));
1006        for bits in [8usize, 16, 32, 64, 128, 256] {
1007            assert!(
1008                !seeded(DynSolType::Uint(bits), U256::from(4)),
1009                "uint8(250)+10 must not seed 4"
1010            );
1011            assert!(
1012                !seeded(DynSolType::Uint(bits), U256::from(144)),
1013                "uint8(200)*2 must not seed 144"
1014            );
1015            assert!(
1016                !seeded(DynSolType::Uint(bits), U256::from(255)),
1017                "uint8(1)-2 must not seed 255"
1018            );
1019            assert!(
1020                !seeded(DynSolType::Uint(bits), U256::from(232)),
1021                "uint8(10)**3 must not seed 232"
1022            );
1023        }
1024        let neg_56 = I256::try_from(-56).unwrap().into_raw();
1025        let neg_128 = I256::try_from(-128).unwrap().into_raw();
1026        for bits in [8usize, 16, 32, 64, 128, 256] {
1027            assert!(!seeded(DynSolType::Int(bits), neg_56), "int8(100)+100 must not seed -56");
1028            assert!(!seeded(DynSolType::Int(bits), neg_128), "int8(64)*2 must not seed -128");
1029        }
1030
1031        // The in-range expression and the cast operands themselves still fold.
1032        assert_word(&map, DynSolType::Int(8), B256::from(U256::from(125)), "int8(5) ** 3 -> 125");
1033        assert_word(&map, DynSolType::Uint(8), B256::from(U256::from(250)), "operand uint8(250)");
1034    }
1035
1036    #[test]
1037    fn test_checked_negation_overflow_does_not_seed() {
1038        // Negating `type(intN).min` overflows the `intN` type and reverts under checked arithmetic,
1039        // so the (wrapped) min value must not be re-seeded; in-range negations still fold.
1040        let source = r#"
1041        contract C {
1042            int8 constant A = -type(int8).min + 1; // -(-128) reverts, must not seed -127
1043            int8 constant B = -int8(1);            // -1, folds normally
1044        }"#;
1045        let map = process_source_literals(source);
1046
1047        let neg_127 = I256::try_from(-127).unwrap().into_raw();
1048        let seeded =
1049            |ty, raw: U256| map.words.get(&ty).is_some_and(|s| s.contains(&B256::from(raw)));
1050        for bits in [8usize, 16, 32, 64, 128, 256] {
1051            assert!(
1052                !seeded(DynSolType::Int(bits), neg_127),
1053                "-type(int8).min + 1 must not seed -127"
1054            );
1055        }
1056        let neg_one = B256::from(I256::try_from(-1).unwrap().into_raw());
1057        assert_word(&map, DynSolType::Int(8), neg_one, "-int8(1) -> -1");
1058    }
1059
1060    #[test]
1061    fn test_fixed_bytes_folding() {
1062        // `bytesN` is left-aligned; truncation keeps the leftmost bytes (and must not zero the
1063        // word), widening pads on the right.
1064        let source = r#"
1065        contract C {
1066            bytes4 constant A = bytes4(uint256(0xdeadbeef12345678)); // low 4 bytes, not zero
1067            bytes2 constant B = bytes2(bytes4(uint32(0x12345678)));  // keep left -> 0x1234
1068            bytes4 constant D = bytes4(bytes2(uint16(0x1234)));      // pad right -> 0x12340000
1069        }"#;
1070        let map = process_source_literals(source);
1071
1072        let low4 = B256::right_padding_from(&[0x12, 0x34, 0x56, 0x78]);
1073        assert_word(&map, DynSolType::FixedBytes(4), low4, "bytes4 must not fold to zero");
1074        let left2 = B256::right_padding_from(&[0x12, 0x34]);
1075        assert_word(&map, DynSolType::FixedBytes(2), left2, "bytes2(bytes4(..)) keeps left bytes");
1076        let padded = B256::right_padding_from(&[0x12, 0x34, 0x00, 0x00]);
1077        assert_word(&map, DynSolType::FixedBytes(4), padded, "bytes4(bytes2(..)) pads right");
1078    }
1079
1080    #[test]
1081    fn test_type_min_max_folding() {
1082        let source = r#"
1083        contract C {
1084            uint256 constant A = type(uint256).max;
1085            uint8 constant B = type(uint8).max;        // 255
1086            int256 constant D = type(int256).min;
1087            int256 constant E = type(int256).max;
1088            uint256 constant F = type(uint256).max - 1;
1089            int8 constant G = type(int8).min;          // -128
1090            int24 constant H = type(int24).min;        // -2**23
1091        }"#;
1092        let map = process_source_literals(source);
1093
1094        assert_word(&map, DynSolType::Uint(256), B256::from(U256::MAX), "type(uint256).max");
1095        assert_word(&map, DynSolType::Uint(8), B256::from(U256::from(255)), "type(uint8).max");
1096        assert_word(
1097            &map,
1098            DynSolType::Int(256),
1099            B256::from(I256::MIN.into_raw()),
1100            "type(int256).min",
1101        );
1102        assert_word(
1103            &map,
1104            DynSolType::Int(256),
1105            B256::from(I256::MAX.into_raw()),
1106            "type(int256).max",
1107        );
1108        let max_minus_one = B256::from(U256::MAX - U256::from(1));
1109        assert_word(&map, DynSolType::Uint(256), max_minus_one, "type(uint256).max - 1");
1110        let min8 = B256::from(I256::try_from(-128).unwrap().into_raw());
1111        assert_word(&map, DynSolType::Int(8), min8, "type(int8).min -> -128");
1112        let min24 = B256::from(I256::try_from(-(1i64 << 23)).unwrap().into_raw());
1113        assert_word(&map, DynSolType::Int(24), min24, "type(int24).min -> -2**23");
1114    }
1115
1116    #[test]
1117    fn test_address_cast_seeds_address_type() {
1118        // A folded `address(...)` cast must be seeded under `address`, not leak into a `uint160`
1119        // bucket.
1120        let source = r#"
1121        contract C {
1122            address constant A = address(4660);
1123        }"#;
1124        let map = process_source_literals(source);
1125
1126        assert_word(
1127            &map,
1128            DynSolType::Address,
1129            B256::from(low_bits(U256::from(4660), 160)),
1130            "address(4660) -> address",
1131        );
1132        assert_eq!(map.words.get(&DynSolType::Uint(160)), None, "must not seed a uint160 bucket");
1133    }
1134
1135    #[test]
1136    fn test_max_values_is_respected() {
1137        // Each string literal seeds up to 3 values (hash, padded, and the string itself), so a
1138        // limit of 2 must stop collection mid-string rather than overrun.
1139        let source = r#"
1140        contract C {
1141            string constant A = "aaa";
1142            string constant B = "bbb";
1143            string constant D = "ccc";
1144        }"#;
1145        let map = process_source_literals_with_max(source, 2);
1146
1147        let total = map.words.values().map(|set| set.len()).sum::<usize>()
1148            + map.strings.len()
1149            + map.bytes.len();
1150        assert!(total <= 2, "max_values not respected: collected {total} values");
1151    }
1152
1153    // -- TEST HELPERS ---------------------------------------------------------
1154
1155    fn process_source_literals(source: &str) -> LiteralMaps {
1156        process_source_literals_with_max(source, usize::MAX)
1157    }
1158
1159    fn process_source_literals_with_max(source: &str, max_values: usize) -> LiteralMaps {
1160        let mut compiler =
1161            solar::sema::Compiler::new(Session::builder().with_stderr_emitter().build());
1162        compiler
1163            .enter_mut(|c| -> std::io::Result<()> {
1164                let mut pcx = c.parse();
1165                pcx.set_resolve_imports(false);
1166
1167                pcx.add_file(
1168                    c.sess().source_map().new_source_file(source_map::FileName::Stdin, source)?,
1169                );
1170                pcx.parse();
1171                let _ = c.lower_asts();
1172                Ok(())
1173            })
1174            .expect("Failed to compile test source");
1175
1176        LiteralsCollector::process(&std::sync::Arc::new(compiler), None, max_values)
1177    }
1178
1179    fn assert_word(literals: &LiteralMaps, ty: DynSolType, value: B256, msg: &str) {
1180        assert!(literals.words.get(&ty).is_some_and(|set| set.contains(&value)), "{}", msg);
1181    }
1182}