Skip to main content

chisel/
executor.rs

1//! Executor
2//!
3//! This module contains the execution logic for the [SessionSource].
4
5use crate::prelude::{ChiselDispatcher, ChiselResult, ChiselRunner, SessionSource, SolidityHelper};
6use alloy_dyn_abi::{DynSolType, DynSolValue};
7use alloy_json_abi::EventParam;
8use alloy_primitives::{Address, B256, U256, hex};
9use eyre::{Result, WrapErr};
10use foundry_compilers::Artifact;
11use foundry_evm::{
12    backend::Backend,
13    core::evm::FoundryEvmNetwork,
14    decode::decode_console_logs,
15    inspectors::CheatsConfig,
16    opts::{ExecutionSpecContext, resolve_execution_spec},
17    traces::TraceRequirements,
18};
19use solar::{
20    ast::{ElementaryType, LitKind, StmtKind as AstStmtKind, StrKind, UnOpKind, yul},
21    interface::Session,
22    sema::{
23        hir::{Event, Expr, ExprKind, StmtKind},
24        ty::{Gcx, Ty, TyKind},
25    },
26};
27use std::ops::ControlFlow;
28use yansi::Paint;
29
30/// Result of inspecting a Solidity snippet.
31#[derive(Debug)]
32pub struct InspectResult {
33    /// Whether the input was fully handled by inspection.
34    pub control_flow: ControlFlow<()>,
35    /// The formatted value to display, if any.
36    pub formatted_output: Option<String>,
37    /// An expression that recreates the inspected value, if any.
38    pub last_result: Option<String>,
39    /// Whether the previous reusable result should be cleared.
40    pub clear_last_result: bool,
41    /// Input to execute and persist after inspection, if it differs from the original input.
42    pub replay_input: Option<String>,
43}
44
45impl InspectResult {
46    const fn empty(control_flow: ControlFlow<()>) -> Self {
47        Self {
48            control_flow,
49            formatted_output: None,
50            last_result: None,
51            clear_last_result: false,
52            replay_input: None,
53        }
54    }
55}
56
57struct YulInspection {
58    inspector_input: String,
59    replay_input: String,
60}
61
62fn yul_inspection(input: &str, session_source: &str) -> Option<YulInspection> {
63    let sess = Session::builder().with_buffer_emitter(Default::default()).build();
64    sess.enter_sequential(|| {
65        let arena = solar::ast::Arena::new();
66        let mut parser = solar::parse::Parser::from_source_code(
67            &sess,
68            &arena,
69            "ChiselInput.sol".to_string().into(),
70            input,
71        )
72        .ok()?;
73        let stmt = parser.parse_stmt().map_err(|err| err.emit()).ok()?;
74        if !parser.token.is_eof() {
75            return None;
76        }
77        let AstStmtKind::Assembly(assembly) = &stmt.kind else { return None };
78        let last = assembly.block.stmts.last()?;
79        let yul::StmtKind::Expr(expr) = &last.kind else { return None };
80
81        let expr_range = sess.source_map().span_to_source(expr.span).ok()?.data;
82        let expression = input.get(expr_range.clone())?;
83        let result_var = std::iter::once("__chisel_yul_result".to_string())
84            .chain((1..).map(|suffix| format!("__chisel_yul_result_{suffix}")))
85            .find(|name| !input.contains(name) && !session_source.contains(name))?;
86
87        let mut assembly = input.to_string();
88        assembly.replace_range(expr_range.clone(), &format!("{result_var} := {expression}"));
89        let inspector_input = format!(
90            "uint256 {result_var}; {assembly}\nbytes memory inspectoor = abi.encode({result_var});"
91        );
92
93        let mut replay_input = input.to_string();
94        replay_input.replace_range(expr_range, &format!("pop({expression})"));
95        Some(YulInspection { inspector_input, replay_input })
96    })
97}
98
99/// Executor implementation for [SessionSource]
100impl<FEN: FoundryEvmNetwork> SessionSource<FEN> {
101    /// Runs the source with the [ChiselRunner]
102    pub async fn execute(&mut self) -> Result<ChiselResult> {
103        eyre::ensure!(
104            !self.config.fork_url_required || self.config.evm_opts.fork_url.is_some(),
105            "this saved Chisel session requires a current fork endpoint before execution"
106        );
107        // Recompile the project and ensure no errors occurred.
108        let output = self.build()?;
109
110        let (bytecode, final_pc) = output.enter(|output| -> Result<_> {
111            let contract = output
112                .repl_contract()
113                .ok_or_else(|| eyre::eyre!("failed to find REPL contract"))?;
114            trace!(?contract, "REPL contract");
115            let bytecode = contract
116                .get_bytecode_bytes()
117                .ok_or_else(|| eyre::eyre!("No bytecode found for `REPL` contract"))?;
118            Ok((bytecode.into_owned(), output.final_pc(contract)?))
119        })?;
120        let final_pc = final_pc.unwrap_or_default();
121        let mut runner = self.build_runner(final_pc).await?;
122        runner.run(bytecode)
123    }
124
125    /// Inspect a contract element inside of the current session
126    ///
127    /// ### Takes
128    ///
129    /// A solidity snippet
130    ///
131    /// ### Returns
132    ///
133    /// If the input is valid, returns its [`InspectResult`].
134    pub async fn inspect(&self, input: &str) -> Result<InspectResult> {
135        let line = format!("bytes memory inspectoor = abi.encode({input});");
136        let (mut source, replay_input) = match self.clone_with_new_line(line) {
137            Ok((source, _)) => (source, None),
138            Err(err) => {
139                debug!(%err, "failed to build new source for inspection");
140                let Some(inspection) = yul_inspection(input, &self.to_repl_source()) else {
141                    return Ok(InspectResult::empty(ControlFlow::Continue(())));
142                };
143                if self
144                    .clone_with_new_line(input.to_string())
145                    .is_ok_and(|(source, _)| source.build().is_ok())
146                {
147                    return Ok(InspectResult::empty(ControlFlow::Continue(())));
148                }
149                let Ok((source, _)) = self.clone_with_new_line(inspection.inspector_input) else {
150                    return Ok(InspectResult::empty(ControlFlow::Continue(())));
151                };
152                (source, Some(inspection.replay_input))
153            }
154        };
155
156        let mut source_without_inspector = self.clone();
157
158        // Events and tuples fails compilation due to it not being able to be encoded in
159        // `inspectoor`. If that happens, try executing without the inspector.
160        let (mut res, err) = match source.execute().await {
161            Ok(res) => (res, None),
162            Err(err) => {
163                debug!(?err, %input, "execution failed");
164                let should_execute = self
165                    .clone_with_new_line(input.to_string())
166                    .ok()
167                    .and_then(|(source, do_execute)| {
168                        if !do_execute {
169                            return None;
170                        }
171                        source.build().ok().map(|output| {
172                            output.enter(|output| {
173                                let body = output.run_func_body();
174                                let Some(last) = body.last() else { return false };
175                                let StmtKind::Expr(expr) = last.kind else { return false };
176                                should_continue(expr)
177                            })
178                        })
179                    })
180                    .unwrap_or(false);
181                if should_execute {
182                    return Ok(InspectResult::empty(ControlFlow::Continue(())));
183                }
184                match source_without_inspector.execute().await {
185                    Ok(res) => (res, Some(err)),
186                    Err(_) => {
187                        if self.config.foundry_config.verbosity >= 3 {
188                            sh_err!("Could not inspect: {err}")?;
189                        }
190                        return Ok(InspectResult::empty(ControlFlow::Continue(())));
191                    }
192                }
193            }
194        };
195
196        // If abi-encoding the input failed, check whether it is an event
197        if let Some(err) = err {
198            let output = source_without_inspector.build()?;
199
200            let formatted_event = output.enter(|output| {
201                let gcx = output.gcx();
202                output.get_event(input).map(|eid| format_event_definition(gcx, gcx.hir.event(eid)))
203            });
204            if let Some(formatted_event) = formatted_event {
205                return Ok(InspectResult {
206                    control_flow: ControlFlow::Break(()),
207                    formatted_output: Some(formatted_event?),
208                    last_result: None,
209                    clear_last_result: false,
210                    replay_input: None,
211                });
212            }
213
214            // we were unable to check the event
215            if self.config.foundry_config.verbosity >= 3 {
216                sh_err!("Failed eval: {err}")?;
217            }
218
219            debug!(%err, %input, "failed abi encode input");
220            return Ok(InspectResult::empty(ControlFlow::Break(())));
221        }
222        drop(source_without_inspector);
223
224        let Some((stack, memory)) = &res.state else {
225            // Show traces and logs, if there are any, and return an error
226            if let Ok(decoder) = ChiselDispatcher::decode_traces(&source.config, &mut res).await {
227                ChiselDispatcher::<FEN>::show_traces(&decoder, &mut res).await?;
228            }
229            let decoded_logs = decode_console_logs(&res.logs);
230            if !decoded_logs.is_empty() {
231                sh_println!("{}", "Logs:".green())?;
232                for log in decoded_logs {
233                    sh_println!("  {log}")?;
234                }
235            }
236
237            return Err(eyre::eyre!("Failed to inspect expression"));
238        };
239
240        // Either the expression referred to by `input`, or the last expression,
241        // which was wrapped in `abi.encode`.
242        let generated_output = source.build()?;
243
244        // Inside the compiler closure, infer the DynSolType of the inspected expression and
245        // determine whether the REPL should continue.
246        let res_ty = generated_output.enter(|out| -> Option<(bool, DynSolType)> {
247            let gcx = out.gcx();
248
249            // Find the appended `bytes memory inspectoor = abi.encode(<input>);` and pull out the
250            // first call argument.
251            let block = out.run_func_body();
252            let last = block.last()?;
253            let StmtKind::DeclSingle(vid) = last.kind else { return None };
254            let var = gcx.hir.variable(vid);
255            let init = var.initializer?;
256            let ExprKind::Call(_callee, args) = &init.kind else { return None };
257            let inner_expr = args.exprs().next()?;
258
259            let ty = expr_to_dyn(gcx, inner_expr)?;
260            Some((should_continue(inner_expr), ty))
261        });
262
263        let Some((cont, ty)) = res_ty else {
264            return Ok(InspectResult::empty(ControlFlow::Continue(())));
265        };
266
267        // the file compiled correctly, thus the last stack item must be the memory offset of
268        // the `bytes memory inspectoor` value
269        let data = (|| -> Option<_> {
270            let mut offset: usize = stack.last()?.try_into().ok()?;
271            debug!("inspect memory @ {offset}: {}", hex::encode(memory));
272            let mem_offset = memory.get(offset..offset + 32)?;
273            let len: usize = U256::try_from_be_slice(mem_offset)?.try_into().ok()?;
274            offset += 32;
275            memory.get(offset..offset + len)
276        })();
277        let Some(data) = data else {
278            eyre::bail!("Failed to inspect last expression: could not retrieve data from memory");
279        };
280        let clear_last_result = dyn_ty_contains_function(&ty);
281        let last_result = (!clear_last_result)
282            .then(|| format!("abi.decode(hex\"{}\", ({ty}))", hex::encode(data)));
283        let token = ty.abi_decode(data).wrap_err("Could not decode inspected values")?;
284        let c = if cont || replay_input.is_some() {
285            ControlFlow::Continue(())
286        } else {
287            ControlFlow::Break(())
288        };
289        Ok(InspectResult {
290            control_flow: c,
291            formatted_output: Some(format_token(token)),
292            last_result,
293            clear_last_result,
294            replay_input,
295        })
296    }
297
298    async fn build_runner(&mut self, final_pc: usize) -> Result<ChiselRunner<FEN>> {
299        let backend = if let Some(backend) = &self.config.cached_backend {
300            backend.clone()
301        } else {
302            let opts = &self.config.evm_opts;
303            let backend = Backend::spawn(opts.get_fork(
304                &self.config.foundry_config,
305                opts.env.chain_id.unwrap_or_default(),
306                None,
307            ))?;
308            self.config.cached_backend = Some(backend.clone());
309            backend
310        };
311        let (mut evm_env, tx_env) = backend.env(&self.config.evm_opts).await?;
312        let fork_context = backend.fork()?.as_ref().map(|fork| fork.context());
313        let fork_chain_id = fork_context.map(|context| context.source_chain_id);
314        let fork_hardfork = fork_context.and_then(|context| context.hardfork);
315        self.config.source_chain_id = fork_chain_id;
316        self.config.resolved_hardfork = resolve_execution_spec(
317            self.config.foundry_config.evm_version,
318            self.config.foundry_config.hardfork,
319            &mut evm_env,
320            ExecutionSpecContext::local_or_fork(fork_chain_id, fork_hardfork),
321            None,
322        );
323
324        let executor = self
325            .config
326            .executor_builder
327            .clone()
328            .inspectors(|stack| {
329                stack
330                    .logs(self.config.foundry_config.live_logs)
331                    .chisel_state(final_pc)
332                    .trace_requirements(TraceRequirements::none().with_calls(true))
333                    .cheatcodes(
334                        CheatsConfig::new(
335                            &self.config.foundry_config,
336                            self.config.evm_opts.clone(),
337                            None,
338                            None,
339                            false,
340                        )
341                        .into(),
342                    )
343            })
344            .gas_limit(self.config.evm_opts.gas_limit())
345            .legacy_assertions(self.config.foundry_config.legacy_assertions)
346            .build(evm_env, tx_env, backend, self.config.evm_opts.networks);
347
348        Ok(ChiselRunner::new(executor, U256::MAX, Address::ZERO, self.config.calldata.clone()))
349    }
350}
351
352/// Formats a value into an inspection message
353// TODO: Verbosity option
354fn format_token(token: DynSolValue) -> String {
355    match token {
356        DynSolValue::Address(a) => {
357            format!("Type: {}\n└ Data: {}", "address".red(), a.cyan())
358        }
359        DynSolValue::Function(f) => {
360            let (address, selector) = f.as_address_and_selector();
361            format!(
362                "Type: {}\n├ Address: {}\n└ Selector: {}",
363                "function".red(),
364                address.cyan(),
365                selector.cyan()
366            )
367        }
368        DynSolValue::FixedBytes(b, byte_len) => {
369            format!(
370                "Type: {}\n└ Data: {}",
371                format!("bytes{byte_len}").red(),
372                hex::encode_prefixed(b).cyan()
373            )
374        }
375        DynSolValue::Int(i, bit_len) => {
376            format!(
377                "Type: {}\n├ Hex: {}\n├ Hex (full word): {}\n└ Decimal: {}",
378                format!("int{bit_len}").red(),
379                format!(
380                    "0x{}",
381                    format!("{i:x}")
382                        .chars()
383                        .skip(if i.is_negative() { 64 - bit_len / 4 } else { 0 })
384                        .collect::<String>()
385                )
386                .cyan(),
387                hex::encode_prefixed(B256::from(i)).cyan(),
388                i.cyan()
389            )
390        }
391        DynSolValue::Uint(i, bit_len) => {
392            format!(
393                "Type: {}\n├ Hex: {}\n├ Hex (full word): {}\n└ Decimal: {}",
394                format!("uint{bit_len}").red(),
395                format!("0x{i:x}").cyan(),
396                hex::encode_prefixed(B256::from(i)).cyan(),
397                i.cyan()
398            )
399        }
400        DynSolValue::Bool(b) => {
401            format!("Type: {}\n└ Value: {}", "bool".red(), b.cyan())
402        }
403        DynSolValue::Bytes(bytes) => {
404            format!(
405                "Type: {}\n└ Data: {}",
406                "dynamic bytes".red(),
407                hex::encode_prefixed(bytes).cyan()
408            )
409        }
410        token @ DynSolValue::String(_) => {
411            let hex = hex::encode(token.abi_encode());
412            let s = token.as_str().expect("matched string value");
413            format!(
414                "Type: {}\n├ UTF-8: {}\n├ Hex (Memory):\n├─ Length ({}): {}\n├─ Contents ({}): {}\n├ Hex (Tuple Encoded):\n├─ Pointer ({}): {}\n├─ Length ({}): {}\n└─ Contents ({}): {}",
415                "string".red(),
416                s.cyan(),
417                "[0x00:0x20]".yellow(),
418                format!("0x{}", &hex[64..128]).cyan(),
419                "[0x20:..]".yellow(),
420                format!("0x{}", &hex[128..]).cyan(),
421                "[0x00:0x20]".yellow(),
422                format!("0x{}", &hex[..64]).cyan(),
423                "[0x20:0x40]".yellow(),
424                format!("0x{}", &hex[64..128]).cyan(),
425                "[0x40:..]".yellow(),
426                format!("0x{}", &hex[128..]).cyan(),
427            )
428        }
429        DynSolValue::FixedArray(tokens) | DynSolValue::Array(tokens) => {
430            let mut out = format!(
431                "{}({}) = {}",
432                "array".red(),
433                format!("{}", tokens.len()).yellow(),
434                '['.red()
435            );
436            for token in tokens {
437                out.push_str("\n  ├ ");
438                out.push_str(&format_token(token).replace('\n', "\n  "));
439                out.push('\n');
440            }
441            out.push_str(&']'.red().to_string());
442            out
443        }
444        DynSolValue::Tuple(tokens) => {
445            let displayed_types = tokens
446                .iter()
447                .map(|t| t.sol_type_name().unwrap_or_default())
448                .collect::<Vec<_>>()
449                .join(", ");
450            let mut out =
451                format!("{}({}) = {}", "tuple".red(), displayed_types.yellow(), '('.red());
452            for token in tokens {
453                out.push_str("\n  ├ ");
454                out.push_str(&format_token(token).replace('\n', "\n  "));
455                out.push('\n');
456            }
457            out.push_str(&')'.red().to_string());
458            out
459        }
460        _ => {
461            unimplemented!()
462        }
463    }
464}
465
466/// Formats an [`Event`] into an inspection message.
467// TODO: Verbosity option
468fn format_event_definition(gcx: Gcx<'_>, event: &Event<'_>) -> Result<String> {
469    let event_name = event.name.as_str().to_string();
470    let inputs = event
471        .parameters
472        .iter()
473        .map(|&pid| {
474            let var = gcx.hir.variable(pid);
475            let name =
476                var.name.map(|n| n.as_str().to_string()).unwrap_or_else(|| "<anonymous>".into());
477            let kind = solar_ty_to_dyn(gcx, gcx.type_of_item(pid.into()))
478                .ok_or_else(|| eyre::eyre!("Invalid type in event {event_name}"))?;
479            Ok(EventParam {
480                name,
481                ty: kind.to_string(),
482                components: vec![],
483                indexed: var.indexed,
484                internal_type: None,
485            })
486        })
487        .collect::<Result<Vec<_>>>()?;
488    let event = alloy_json_abi::Event { name: event_name, inputs, anonymous: event.anonymous };
489
490    Ok(format!(
491        "Type: {}\n├ Name: {}\n├ Signature: {:?}\n└ Selector: {:?}",
492        "event".red(),
493        SolidityHelper::new().highlight(&format!(
494            "{}({})",
495            event.name,
496            event
497                .inputs
498                .iter()
499                .map(|param| format!(
500                    "{}{}{}",
501                    param.ty,
502                    if param.indexed { " indexed" } else { "" },
503                    if param.name.is_empty() {
504                        String::default()
505                    } else {
506                        format!(" {}", param.name)
507                    },
508                ))
509                .collect::<Vec<_>>()
510                .join(", ")
511        )),
512        event.signature().cyan(),
513        event.selector().cyan(),
514    ))
515}
516
517/// Converts an [`Expr`] directly to a [`DynSolType`] for ABI inspection.
518fn expr_to_dyn(gcx: Gcx<'_>, expr: &Expr<'_>) -> Option<DynSolType> {
519    gcx.type_of_expr(expr.id).and_then(|ty| solar_expr_ty_to_dyn(gcx, ty, expr))
520}
521
522/// Whether execution should continue after inspecting this expression.
523#[inline]
524fn should_continue(expr: &Expr<'_>) -> bool {
525    match &expr.kind {
526        // assignments and compound assignments
527        ExprKind::Assign(_, _, _) => true,
528        // Delete expressions.
529        ExprKind::Delete(_) => true,
530        // ++/-- pre/post operations
531        ExprKind::Unary(op, _) => matches!(
532            op.kind,
533            UnOpKind::PreInc | UnOpKind::PreDec | UnOpKind::PostInc | UnOpKind::PostDec
534        ),
535        // Array.pop()
536        ExprKind::Call(callee, _) => match &callee.kind {
537            ExprKind::Member(_, ident) => ident.as_str() == "pop",
538            _ => false,
539        },
540        _ => false,
541    }
542}
543
544/// Maps a solar [`ElementaryType`] to a [`DynSolType`].
545const fn elementary_to_dyn(et: ElementaryType) -> Option<DynSolType> {
546    Some(match et {
547        ElementaryType::Address(_) => DynSolType::Address,
548        ElementaryType::Bool => DynSolType::Bool,
549        ElementaryType::String => DynSolType::String,
550        ElementaryType::Bytes => DynSolType::Bytes,
551        ElementaryType::Int(size) => DynSolType::Int(size.bits() as usize),
552        ElementaryType::UInt(size) => DynSolType::Uint(size.bits() as usize),
553        ElementaryType::FixedBytes(size) => DynSolType::FixedBytes(size.bytes() as usize),
554        // Fixed-point numbers are not yet representable as DynSolType.
555        ElementaryType::Fixed(_, _) | ElementaryType::UFixed(_, _) => return None,
556    })
557}
558
559fn dyn_ty_contains_function(ty: &DynSolType) -> bool {
560    match ty {
561        DynSolType::Function => true,
562        DynSolType::Array(inner) | DynSolType::FixedArray(inner, _) => {
563            dyn_ty_contains_function(inner)
564        }
565        DynSolType::Tuple(types) => types.iter().any(dyn_ty_contains_function),
566        _ => false,
567    }
568}
569
570/// Maps a solar [`Ty`] to a [`DynSolType`].
571fn solar_expr_ty_to_dyn<'gcx>(gcx: Gcx<'gcx>, ty: Ty<'gcx>, expr: &Expr<'_>) -> Option<DynSolType> {
572    // `expr` is the inspected expression inside Chisel's generated `abi.encode(...)` call. Solar
573    // currently reports hex string literals as `StringLiteral`, but solc ABI-encodes
574    // `hex"..."` literals as dynamic bytes in that context.
575    let expr = expr.peel_parens();
576    if matches!(expr.kind, ExprKind::Lit(lit) if matches!(lit.kind, LitKind::Str(StrKind::Hex, ..)))
577    {
578        return Some(DynSolType::Bytes);
579    }
580
581    solar_ty_to_dyn(gcx, ty)
582}
583
584fn solar_ty_to_dyn<'gcx>(gcx: Gcx<'gcx>, ty: Ty<'gcx>) -> Option<DynSolType> {
585    match ty.kind {
586        TyKind::Elementary(et) => elementary_to_dyn(et),
587        TyKind::Ref(inner, _) => solar_ty_to_dyn(gcx, inner),
588        TyKind::Array(elem, n) => {
589            let inner = solar_ty_to_dyn(gcx, elem)?;
590            let size: usize = n.try_into().ok()?;
591            Some(DynSolType::FixedArray(Box::new(inner), size))
592        }
593        TyKind::DynArray(elem) => {
594            let inner = solar_ty_to_dyn(gcx, elem)?;
595            Some(DynSolType::Array(Box::new(inner)))
596        }
597        TyKind::Slice(array) => solar_ty_to_dyn(gcx, array),
598        TyKind::Tuple(tys) => {
599            Some(DynSolType::Tuple(tys.iter().filter_map(|t| solar_ty_to_dyn(gcx, *t)).collect()))
600        }
601        TyKind::Mapping(_, _) => None,
602        TyKind::Struct(sid) => Some(DynSolType::Tuple(
603            gcx.struct_field_types(sid).iter().filter_map(|t| solar_ty_to_dyn(gcx, *t)).collect(),
604        )),
605        TyKind::Enum(_) => Some(DynSolType::Uint(8)),
606        TyKind::Udvt(inner, _) => solar_ty_to_dyn(gcx, inner),
607        TyKind::Contract(_) => Some(DynSolType::Address),
608        TyKind::Fn(f) if f.is_external() => Some(DynSolType::Function),
609        TyKind::Fn(_) => None,
610        TyKind::Type(inner) => solar_ty_to_dyn(gcx, inner),
611        TyKind::Meta(inner) => solar_ty_to_dyn(gcx, inner),
612        TyKind::IntLiteral(neg, size, _) => {
613            let bits = (size.bits() as usize).max(8);
614            // Round up to the nearest multiple of 8 bits, capped at 256.
615            let bits = bits.div_ceil(8) * 8;
616            let bits = bits.min(256);
617            if neg {
618                Some(DynSolType::Int(bits.max(8)))
619            } else {
620                Some(DynSolType::Uint(bits.max(8)))
621            }
622        }
623        TyKind::StringLiteral(valid_utf8, _) => {
624            if valid_utf8 {
625                Some(DynSolType::String)
626            } else {
627                Some(DynSolType::Bytes)
628            }
629        }
630        TyKind::Module(_)
631        | TyKind::BuiltinModule(_)
632        | TyKind::Error(_, _)
633        | TyKind::Event(_, _)
634        | TyKind::Err(_) => None,
635        _ => None,
636    }
637}
638
639#[cfg(test)]
640mod tests {
641    use super::*;
642    use crate::source::SessionSourceConfig;
643    use foundry_compilers::{error::SolcError, solc::Solc};
644    use foundry_config::Config;
645    use foundry_evm::{core::evm::EthEvmNetwork, opts::EvmOpts};
646    use foundry_evm_networks::{NetworkConfigs, celo::transfer::CELO_TRANSFER_ADDRESS};
647    use foundry_test_utils::util::SOLC_VERSION;
648    use solar::sema::Compiler;
649    use std::sync::Mutex;
650
651    #[cfg(feature = "monad")]
652    use foundry_evm::{
653        core::{constants::MONAD_CHEATCODE_ADDRESS, evm::MonadEvmNetwork},
654        executors::ExecutorBuilder,
655    };
656
657    type TestSessionSource = SessionSource<EthEvmNetwork>;
658
659    #[tokio::test]
660    async fn saved_fork_cannot_execute_without_a_current_endpoint() {
661        let dir = tempfile::tempdir().unwrap();
662        let mut source = TestSessionSource::new(SessionSourceConfig {
663            foundry_config: Config {
664                solc: Some(foundry_config::SolcReq::Local(dir.path().join("missing-solc"))),
665                ..Default::default()
666            },
667            fork_url_required: true,
668            no_vm: true,
669            ..Default::default()
670        })
671        .unwrap();
672
673        let error = source.execute().await.unwrap_err();
674
675        assert_eq!(
676            error.to_string(),
677            "this saved Chisel session requires a current fork endpoint before execution"
678        );
679    }
680
681    async fn assert_celo_transfer_precompile(config: SessionSourceConfig<EthEvmNetwork>) {
682        let mut source = SessionSource::<EthEvmNetwork>::new(config).unwrap();
683        let mut runner = source.build_runner(0).await.unwrap();
684        let from = Address::with_last_byte(1);
685        let to = Address::with_last_byte(2);
686        let amount = U256::from(4);
687        runner.executor.set_balance(from, U256::from(10)).unwrap();
688        runner.executor.set_balance(to, U256::ONE).unwrap();
689
690        let mut input = vec![0u8; 96];
691        input[12..32].copy_from_slice(from.as_slice());
692        input[44..64].copy_from_slice(to.as_slice());
693        input[64..96].copy_from_slice(&amount.to_be_bytes::<32>());
694        let result = runner
695            .executor
696            .transact_raw(Address::ZERO, CELO_TRANSFER_ADDRESS, input.into(), U256::ZERO)
697            .unwrap();
698
699        assert!(!result.reverted);
700        assert_eq!(runner.executor.get_balance(from).unwrap(), U256::from(6));
701        assert_eq!(runner.executor.get_balance(to).unwrap(), U256::from(5));
702    }
703
704    #[tokio::test(flavor = "multi_thread")]
705    async fn celo_network_reaches_fresh_and_restored_runners() {
706        let networks = NetworkConfigs::with_celo();
707        let mut evm_opts = EvmOpts { networks, ..Default::default() };
708        evm_opts.env.gas_limit = 30_000_000u64.into();
709        let config = SessionSourceConfig::<EthEvmNetwork> {
710            foundry_config: Config { networks, ..Default::default() },
711            evm_opts,
712            ..Default::default()
713        };
714
715        assert_celo_transfer_precompile(config.clone()).await;
716
717        let encoded = serde_json::to_string(&config).unwrap();
718        let mut restored =
719            serde_json::from_str::<SessionSourceConfig<EthEvmNetwork>>(&encoded).unwrap();
720        restored.initialize_local_context();
721        assert_celo_transfer_precompile(restored).await;
722    }
723
724    #[cfg(feature = "monad")]
725    #[tokio::test(flavor = "multi_thread")]
726    async fn chisel_runner_uses_dispatched_monad_tooling() {
727        let networks = NetworkConfigs::with_monad();
728        let mut source = SessionSource::<MonadEvmNetwork>::new(SessionSourceConfig {
729            foundry_config: Config { networks, ..Default::default() },
730            evm_opts: EvmOpts { networks, ..Default::default() },
731            executor_builder: ExecutorBuilder::<MonadEvmNetwork>::new(),
732            ..Default::default()
733        })
734        .unwrap();
735
736        let runner = source.build_runner(0).await.unwrap();
737        assert_eq!(
738            runner.executor.inspector().extra_cheatcode_addresses(),
739            &[MONAD_CHEATCODE_ADDRESS]
740        );
741    }
742
743    #[test]
744    fn test_expressions() {
745        static EXPRESSIONS: &[(&str, DynSolType)] = {
746            use DynSolType::*;
747            &[
748                // units
749                // uint
750                ("1 seconds", Uint(8)),
751                ("1 minutes", Uint(8)),
752                ("1 hours", Uint(16)),
753                ("1 days", Uint(24)),
754                ("1 weeks", Uint(24)),
755                ("1 wei", Uint(8)),
756                ("1 gwei", Uint(32)),
757                ("1 ether", Uint(64)),
758                // int
759                ("-1 seconds", Int(8)),
760                ("-1 minutes", Int(8)),
761                ("-1 hours", Int(16)),
762                ("-1 days", Int(24)),
763                ("-1 weeks", Int(24)),
764                ("-1 wei", Int(8)),
765                ("-1 gwei", Int(32)),
766                ("-1 ether", Int(64)),
767                //
768                ("true ? 1 : 0", Uint(8)),
769                // misc
770                //
771
772                // ops
773                // uint
774                ("1 + 1", Uint(8)),
775                ("1 - 1", Uint(8)),
776                ("1 * 1", Uint(8)),
777                ("1 / 1", Uint(8)),
778                ("1 % 1", Uint(8)),
779                ("1 ** 1", Uint(8)),
780                ("1 | 1", Uint(8)),
781                ("1 & 1", Uint(8)),
782                ("1 ^ 1", Uint(8)),
783                ("1 >> 1", Uint(8)),
784                ("1 << 1", Uint(8)),
785                // int
786                ("int(1) + 1", Int(256)),
787                ("int(1) - 1", Int(256)),
788                ("int(1) * 1", Int(256)),
789                ("int(1) / 1", Int(256)),
790                ("1 + int(1)", Int(256)),
791                ("1 - int(1)", Int(256)),
792                ("1 * int(1)", Int(256)),
793                ("1 / int(1)", Int(256)),
794                //
795
796                // assign
797                ("uint256 a; a--", Uint(256)),
798                ("uint256 a; --a", Uint(256)),
799                ("uint256 a; a++", Uint(256)),
800                ("uint256 a; ++a", Uint(256)),
801                ("uint256 a; a   = 1", Uint(256)),
802                ("uint256 a; a  += 1", Uint(256)),
803                ("uint256 a; a  -= 1", Uint(256)),
804                ("uint256 a; a  *= 1", Uint(256)),
805                ("uint256 a; a  /= 1", Uint(256)),
806                ("uint256 a; a  %= 1", Uint(256)),
807                ("uint256 a; a  &= 1", Uint(256)),
808                ("uint256 a; a  |= 1", Uint(256)),
809                ("uint256 a; a  ^= 1", Uint(256)),
810                ("uint256 a; a <<= 1", Uint(256)),
811                ("uint256 a; a >>= 1", Uint(256)),
812                //
813
814                // bool
815                ("true && true", Bool),
816                ("true || true", Bool),
817                ("true == true", Bool),
818                ("true != true", Bool),
819                ("!true", Bool),
820                //
821            ]
822        };
823
824        let source = &mut source();
825
826        let array_expressions: &[(&str, DynSolType)] = &[
827            ("[1, 2, 3]", fixed_array(DynSolType::Uint(8), 3)),
828            ("[uint8(1), 2, 3]", fixed_array(DynSolType::Uint(8), 3)),
829            ("[int8(1), 2, 3]", fixed_array(DynSolType::Int(8), 3)),
830            ("new uint256[](3)", array(DynSolType::Uint(256))),
831            ("uint256[] memory a = new uint256[](3);\na[0]", DynSolType::Uint(256)),
832        ];
833        generic_type_test(source, array_expressions);
834        generic_type_test(source, EXPRESSIONS);
835    }
836
837    #[test]
838    fn test_types() {
839        static TYPES: &[(&str, DynSolType)] = {
840            use DynSolType::*;
841            &[
842                // bool
843                ("bool", Bool),
844                ("true", Bool),
845                ("false", Bool),
846                //
847
848                // int and uint
849                ("uint", Uint(256)),
850                ("uint(1)", Uint(256)),
851                ("1", Uint(8)),
852                ("0x01", Uint(8)),
853                ("int", Int(256)),
854                ("int(1)", Int(256)),
855                ("int(-1)", Int(256)),
856                ("-1", Int(8)),
857                ("-0x01", Int(8)),
858                //
859
860                // address
861                ("address", Address),
862                ("address(0)", Address),
863                ("0x690B9A9E9aa1C9dB991C7721a92d351Db4FaC990", Address),
864                ("payable(0)", Address),
865                ("payable(address(0))", Address),
866                //
867
868                // string
869                ("string", String),
870                ("string(\"hello world\")", String),
871                ("\"hello world\"", String),
872                ("unicode\"hello world 😀\"", String),
873                //
874
875                // bytes
876                ("bytes", Bytes),
877                ("bytes(\"hello world\")", Bytes),
878                ("bytes(unicode\"hello world 😀\")", Bytes),
879                ("hex\"68656c6c6f20776f726c64\"", Bytes),
880                //
881            ]
882        };
883
884        let mut types: Vec<(String, DynSolType)> = Vec::with_capacity(96 + 32 + 100);
885        for (n, b) in (8..=256).step_by(8).zip(1..=32) {
886            types.push((format!("uint{n}(0)"), DynSolType::Uint(n)));
887            types.push((format!("int{n}(0)"), DynSolType::Int(n)));
888            types.push((format!("bytes{b}(0x00)"), DynSolType::FixedBytes(b)));
889        }
890
891        for n in 1..=32 {
892            types.push((
893                format!("uint256[{n}]"),
894                DynSolType::FixedArray(Box::new(DynSolType::Uint(256)), n),
895            ));
896        }
897
898        generic_type_test(&mut source(), TYPES);
899        generic_type_test(&mut source(), &types);
900    }
901
902    #[test]
903    fn test_global_vars() {
904        init_tracing();
905
906        // https://docs.soliditylang.org/en/latest/cheatsheet.html#global-variables
907        let global_variables = {
908            use DynSolType::*;
909            &[
910                // abi
911                ("abi.decode(bytes(\"\"), (uint8[13]))", FixedArray(Box::new(Uint(8)), 13)),
912                ("abi.decode(bytes(\"\"), (address, bytes))", Tuple(vec![Address, Bytes])),
913                ("abi.decode(bytes(\"\"), (uint112, uint48))", Tuple(vec![Uint(112), Uint(48)])),
914                ("abi.encode(1, 2)", Bytes),
915                ("abi.encodePacked(uint256(1), uint256(2))", Bytes),
916                ("abi.encodeWithSelector(bytes4(0), 1, 2)", Bytes),
917                ("abi.encodeWithSignature(\"f(uint256)\", 1)", Bytes),
918                //
919
920                //
921                ("bytes.concat()", Bytes),
922                ("bytes.concat(bytes(\"\"))", Bytes),
923                ("bytes.concat(bytes(\"\"), bytes(\"\"))", Bytes),
924                ("string.concat()", String),
925                ("string.concat(\"\")", String),
926                ("string.concat(\"\", \"\")", String),
927                //
928
929                // block
930                ("block.basefee", Uint(256)),
931                ("block.chainid", Uint(256)),
932                ("block.coinbase", Address),
933                ("block.difficulty", Uint(256)),
934                ("block.gaslimit", Uint(256)),
935                ("block.number", Uint(256)),
936                ("block.timestamp", Uint(256)),
937                //
938
939                // tx
940                ("gasleft()", Uint(256)),
941                ("msg.data", Bytes),
942                ("msg.sender", Address),
943                ("msg.sig", FixedBytes(4)),
944                ("msg.value", Uint(256)),
945                ("tx.gasprice", Uint(256)),
946                ("tx.origin", Address),
947                //
948
949                // assertions
950                // assert(bool)
951                // require(bool)
952                // revert()
953                // revert(string)
954                //
955
956                //
957                ("blockhash(0)", FixedBytes(32)),
958                ("keccak256(bytes(\"\"))", FixedBytes(32)),
959                ("sha256(bytes(\"\"))", FixedBytes(32)),
960                ("ripemd160(bytes(\"\"))", FixedBytes(20)),
961                ("ecrecover(bytes32(0), 0, bytes32(0), bytes32(0))", Address),
962                ("addmod(1, 2, 3)", Uint(256)),
963                ("mulmod(1, 2, 3)", Uint(256)),
964                //
965
966                // address
967                ("address(0)", Address),
968                ("address(this)", Address),
969                // ("super", Type::Custom("super".to_string))
970                // (selfdestruct(address payable), None)
971                ("address(0).balance", Uint(256)),
972                ("address(0).code", Bytes),
973                ("address(0).codehash", FixedBytes(32)),
974                ("payable(address(0)).send(1)", Bool),
975                // (address.transfer(uint256), None)
976                //
977
978                // type
979                ("type(C).name", String),
980                ("type(C).creationCode", Bytes),
981                ("type(C).runtimeCode", Bytes),
982                ("type(I).interfaceId", FixedBytes(4)),
983                ("type(uint256).min", Uint(256)),
984                ("type(int128).min", Int(128)),
985                ("type(int256).min", Int(256)),
986                ("type(uint256).max", Uint(256)),
987                ("type(int128).max", Int(128)),
988                ("type(int256).max", Int(256)),
989                ("type(Enum1).min", Uint(8)),
990                ("type(Enum1).max", Uint(8)),
991                // function
992                ("this.run.address", Address),
993                ("this.run.selector", FixedBytes(4)),
994            ]
995        };
996
997        generic_type_test(&mut source(), global_variables);
998    }
999
1000    #[track_caller]
1001    fn source() -> TestSessionSource {
1002        // synchronize solc install
1003        static PRE_INSTALL_SOLC_LOCK: Mutex<bool> = Mutex::new(false);
1004
1005        // on some CI targets installing results in weird malformed solc files, we try installing it
1006        // multiple times
1007        let version = SOLC_VERSION;
1008        for _ in 0..3 {
1009            let mut is_preinstalled = PRE_INSTALL_SOLC_LOCK.lock().unwrap();
1010            if !*is_preinstalled {
1011                let solc = Solc::find_or_install(&version.parse().unwrap())
1012                    .map(|solc| (solc.version.clone(), solc));
1013                match solc {
1014                    Ok((v, solc)) => {
1015                        // successfully installed
1016                        let _ = sh_println!("found installed Solc v{v} @ {}", solc.solc.display());
1017                        break;
1018                    }
1019                    Err(e) => {
1020                        // try reinstalling
1021                        let _ = sh_err!("error while trying to re-install Solc v{version}: {e}");
1022                        let solc = Solc::blocking_install(&version.parse().unwrap());
1023                        if solc.map_err(SolcError::from).is_ok() {
1024                            *is_preinstalled = true;
1025                            break;
1026                        }
1027                    }
1028                }
1029            }
1030        }
1031
1032        SessionSource::new(Default::default()).unwrap()
1033    }
1034
1035    fn array(ty: DynSolType) -> DynSolType {
1036        DynSolType::Array(Box::new(ty))
1037    }
1038
1039    fn fixed_array(ty: DynSolType, len: usize) -> DynSolType {
1040        DynSolType::FixedArray(Box::new(ty), len)
1041    }
1042
1043    /// Lowers the given snippet appended to the REPL contract via solar's HIR pipeline (without
1044    /// invoking solc) and returns the resulting `DynSolType` of the last expression statement in
1045    /// the run() body.
1046    ///
1047    /// Tests bypass `SessionSource::build` (which routes through foundry-compilers + solc) so the
1048    /// Solar type table can be exercised directly without compiling each snippet through solc.
1049    fn get_type_ethabi(s: &mut TestSessionSource, input: &str, clear: bool) -> Option<DynSolType> {
1050        if clear {
1051            s.clear();
1052        }
1053
1054        // Always declare sample types so `type(...)` tests have concrete definitions.
1055        *s = s.clone_with_new_line("enum Enum1 { A }".into()).unwrap().0;
1056        *s = s.clone_with_new_line("contract C {}".into()).unwrap().0;
1057        *s = s.clone_with_new_line("interface I {}".into()).unwrap().0;
1058
1059        let input = format!("{};", input.trim_end().trim_end_matches(';'));
1060        let (new_source, _) = s.clone_with_new_line(input).unwrap();
1061        *s = new_source.clone();
1062
1063        let src = new_source.to_repl_source();
1064        let opts = solar::interface::config::CompileOpts::default();
1065        let sess = solar::interface::Session::builder()
1066            .opts(opts)
1067            .with_buffer_emitter(Default::default())
1068            .build();
1069        let mut compiler = Compiler::new(sess);
1070
1071        compiler.enter_mut(|c| -> Option<DynSolType> {
1072            // Stage 1: parse, lower, and analyze (mutable access required).
1073            let analyzed = {
1074                let mut pcx = c.parse();
1075                let file = c
1076                    .sess()
1077                    .source_map()
1078                    .new_source_file(
1079                        std::path::PathBuf::from(new_source.file_name.clone()),
1080                        src.clone(),
1081                    )
1082                    .ok()?;
1083                pcx.add_file(file);
1084                pcx.parse();
1085                matches!(c.lower_asts(), Ok(ControlFlow::Continue(())))
1086                    && matches!(c.analysis(), Ok(ControlFlow::Continue(())))
1087            };
1088            if !analyzed {
1089                return None;
1090            }
1091
1092            // Stage 2: walk HIR (immutable access).
1093            let gcx = c.gcx();
1094            let hir = &gcx.hir;
1095            let repl = hir.contracts().find(|c| c.name.as_str() == "REPL")?;
1096            let run_fid = repl
1097                .functions()
1098                .find(|&f| hir.function(f).name.as_ref().map(|n| n.as_str()) == Some("run"))?;
1099            let body = hir.function(run_fid).body?;
1100            let last = body.last()?;
1101            let expr = match last.kind {
1102                StmtKind::Expr(e) => e,
1103                _ => return None,
1104            };
1105            expr_to_dyn(gcx, expr)
1106        })
1107    }
1108
1109    fn generic_type_test<'a, T, I>(s: &mut TestSessionSource, input: I)
1110    where
1111        T: AsRef<str> + std::fmt::Display + 'a,
1112        I: IntoIterator<Item = &'a (T, DynSolType)> + 'a,
1113    {
1114        let mut failures = Vec::new();
1115        for (input, expected) in input {
1116            let input = input.as_ref();
1117            let ty = get_type_ethabi(s, input, true);
1118            if ty.as_ref() != Some(expected) {
1119                failures.push(format!("{input}: got {ty:?}, expected {expected:?}"));
1120            }
1121        }
1122        assert!(failures.is_empty(), "\n{}", failures.join("\n"));
1123    }
1124
1125    fn init_tracing() {
1126        let _ = tracing_subscriber::FmtSubscriber::builder()
1127            .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
1128            .try_init();
1129    }
1130}