|
| 1 | +//! Cheatcode information, extracted from the syntactic and semantic analysis of the sources. |
| 2 | +
|
| 3 | +use eyre::{OptionExt, Result}; |
| 4 | +use solar::sema::{self, Compiler, Gcx, hir}; |
| 5 | +use std::{cell::OnceCell, collections::BTreeMap, sync::Arc}; |
| 6 | + |
| 7 | +/// Provides cached, on-demand syntactic and semantic analysis of a completed `Compiler` instance. |
| 8 | +/// |
| 9 | +/// This struct acts as a facade over the `Compiler`, offering lazy-loaded analysis |
| 10 | +/// for tools like cheatcode inspectors. It assumes the compiler has already |
| 11 | +/// completed parsing and lowering. |
| 12 | +/// |
| 13 | +/// # Extending with New Analyses |
| 14 | +/// |
| 15 | +/// To add support for a new type of cached analysis, follow this pattern: |
| 16 | +/// |
| 17 | +/// 1. Add a new `pub OnceCell<Option<T>>` field to `CheatcodeAnalysis`, where `T` is the type of |
| 18 | +/// the data that you are adding support for. |
| 19 | +/// |
| 20 | +/// 2. Implement a getter method for the new field. Inside the getter, use |
| 21 | +/// `self.field.get_or_init()` to compute and cache the value on the first call. |
| 22 | +/// |
| 23 | +/// 3. Inside the closure passed to `get_or_init()`, create a dedicated visitor to traverse the HIR |
| 24 | +/// using `self.compiler.enter()` and collect the required data. |
| 25 | +/// |
| 26 | +/// This ensures all analyses remain lazy, efficient, and consistent with the existing design. |
| 27 | +#[derive(Clone)] |
| 28 | +pub struct CheatcodeAnalysis { |
| 29 | + /// A shared, thread-safe reference to solar's `Compiler` instance. |
| 30 | + pub compiler: Arc<Compiler>, |
| 31 | + |
| 32 | + /// Cached struct definitions in the sources. |
| 33 | + /// Used to keep field order when parsing JSON values. |
| 34 | + pub struct_defs: OnceCell<Option<StructDefinitions>>, |
| 35 | +} |
| 36 | + |
| 37 | +pub type StructDefinitions = BTreeMap<String, Vec<(String, String)>>; |
| 38 | + |
| 39 | +impl std::fmt::Debug for CheatcodeAnalysis { |
| 40 | + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 41 | + f.debug_struct("CheatcodeAnalysis") |
| 42 | + .field("compiler", &"<compiler>") |
| 43 | + .field("struct_defs", &self.struct_defs) |
| 44 | + .finish() |
| 45 | + } |
| 46 | +} |
| 47 | + |
| 48 | +impl CheatcodeAnalysis { |
| 49 | + pub fn new(compiler: Arc<solar::sema::Compiler>) -> Self { |
| 50 | + Self { compiler, struct_defs: OnceCell::new() } |
| 51 | + } |
| 52 | + |
| 53 | + /// Lazily initializes and returns the struct definitions. |
| 54 | + pub fn struct_defs(&self) -> Result<&StructDefinitions> { |
| 55 | + self.struct_defs |
| 56 | + .get_or_init(|| { |
| 57 | + self.compiler.enter(|compiler| { |
| 58 | + let gcx = compiler.gcx(); |
| 59 | + |
| 60 | + StructDefinitionResolver::new(gcx).process().ok() |
| 61 | + }) |
| 62 | + }) |
| 63 | + .as_ref() |
| 64 | + .ok_or_eyre("unable to resolve struct definitions") |
| 65 | + } |
| 66 | +} |
| 67 | + |
| 68 | +/// Generates a map of all struct definitions from the HIR using the resolved `Ty` system. |
| 69 | +pub struct StructDefinitionResolver<'hir> { |
| 70 | + gcx: Gcx<'hir>, |
| 71 | + struct_defs: StructDefinitions, |
| 72 | +} |
| 73 | + |
| 74 | +impl<'hir> StructDefinitionResolver<'hir> { |
| 75 | + /// Constructs a new generator. |
| 76 | + pub fn new(gcx: Gcx<'hir>) -> Self { |
| 77 | + Self { gcx, struct_defs: BTreeMap::new() } |
| 78 | + } |
| 79 | + |
| 80 | + /// Processes the HIR to generate all the struct definitions. |
| 81 | + pub fn process(mut self) -> Result<StructDefinitions> { |
| 82 | + for id in self.hir().strukt_ids() { |
| 83 | + self.resolve_struct_definition(id)?; |
| 84 | + } |
| 85 | + Ok(self.struct_defs) |
| 86 | + } |
| 87 | + |
| 88 | + #[inline] |
| 89 | + fn hir(&self) -> &'hir hir::Hir<'hir> { |
| 90 | + &self.gcx.hir |
| 91 | + } |
| 92 | + |
| 93 | + /// The recursive core of the generator. Resolves a single struct and adds it to the cache. |
| 94 | + fn resolve_struct_definition(&mut self, id: hir::StructId) -> Result<()> { |
| 95 | + let qualified_name = self.get_fully_qualified_name(id); |
| 96 | + if self.struct_defs.contains_key(&qualified_name) { |
| 97 | + return Ok(()); |
| 98 | + } |
| 99 | + |
| 100 | + let hir = self.hir(); |
| 101 | + let strukt = hir.strukt(id); |
| 102 | + let mut fields = Vec::with_capacity(strukt.fields.len()); |
| 103 | + |
| 104 | + for &field_id in strukt.fields { |
| 105 | + let var = hir.variable(field_id); |
| 106 | + let name = |
| 107 | + var.name.ok_or_else(|| eyre::eyre!("struct field is missing a name"))?.to_string(); |
| 108 | + if let Some(ty_str) = self.ty_to_string(self.gcx.type_of_hir_ty(&var.ty)) { |
| 109 | + fields.push((name, ty_str)); |
| 110 | + } |
| 111 | + } |
| 112 | + |
| 113 | + // Only insert if there are fields, to avoid adding empty entries |
| 114 | + if !fields.is_empty() { |
| 115 | + self.struct_defs.insert(qualified_name, fields); |
| 116 | + } |
| 117 | + |
| 118 | + Ok(()) |
| 119 | + } |
| 120 | + |
| 121 | + /// Converts a resolved `Ty` into its canonical string representation. |
| 122 | + fn ty_to_string(&mut self, ty: sema::Ty<'hir>) -> Option<String> { |
| 123 | + let ty = ty.peel_refs(); |
| 124 | + let res = match ty.kind { |
| 125 | + sema::ty::TyKind::Elementary(e) => e.to_string(), |
| 126 | + sema::ty::TyKind::Array(ty, size) => { |
| 127 | + let inner_type = self.ty_to_string(ty)?; |
| 128 | + format!("{inner_type}[{size}]") |
| 129 | + } |
| 130 | + sema::ty::TyKind::DynArray(ty) => { |
| 131 | + let inner_type = self.ty_to_string(ty)?; |
| 132 | + format!("{inner_type}[]") |
| 133 | + } |
| 134 | + sema::ty::TyKind::Struct(id) => { |
| 135 | + // Ensure the nested struct is resolved before proceeding. |
| 136 | + self.resolve_struct_definition(id).ok()?; |
| 137 | + self.get_fully_qualified_name(id) |
| 138 | + } |
| 139 | + sema::ty::TyKind::Udvt(ty, _) => self.ty_to_string(ty)?, |
| 140 | + // For now, map enums to `uint8` |
| 141 | + sema::ty::TyKind::Enum(_) => "uint8".to_string(), |
| 142 | + // For now, map contracts to `address` |
| 143 | + sema::ty::TyKind::Contract(_) => "address".to_string(), |
| 144 | + // Explicitly disallow unsupported types |
| 145 | + _ => return None, |
| 146 | + }; |
| 147 | + |
| 148 | + Some(res) |
| 149 | + } |
| 150 | + |
| 151 | + /// Helper to get the fully qualified name `Contract.Struct`. |
| 152 | + fn get_fully_qualified_name(&self, id: hir::StructId) -> String { |
| 153 | + let hir = self.hir(); |
| 154 | + let strukt = hir.strukt(id); |
| 155 | + if let Some(contract_id) = strukt.contract { |
| 156 | + format!("{}.{}", hir.contract(contract_id).name.as_str(), strukt.name.as_str()) |
| 157 | + } else { |
| 158 | + strukt.name.as_str().into() |
| 159 | + } |
| 160 | + } |
| 161 | +} |
0 commit comments