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