Skip to main content

chisel/
source.rs

1//! Session Source
2//!
3//! This module contains the `SessionSource` struct, which is a minimal wrapper around
4//! the REPL contract's source code. It provides simple compilation, parsing, and
5//! execution helpers.
6
7use eyre::Result;
8use foundry_compilers::{
9    Artifact, ProjectCompileOutput,
10    artifacts::{ConfigurableContractArtifact, Source, Sources},
11    project::ProjectCompiler,
12    solc::Solc,
13};
14use foundry_config::{
15    Config, EtherscanConfigs, FoundryHardfork, RpcEndpoints, SolcReq, cache::CachedEndpoints,
16};
17use foundry_evm::{
18    backend::Backend,
19    core::{bytecode::InstIter, evm::FoundryEvmNetwork},
20    executors::ExecutorBuilder,
21    opts::EvmOpts,
22};
23use foundry_evm_networks::NetworkConfigs;
24use semver::Version;
25use serde::{Deserialize, Deserializer, Serialize, Serializer};
26use solar::{
27    ast::{ItemKind, StmtKind as AstStmtKind, yul},
28    interface::{Span, diagnostics::EmittedDiagnostics},
29    sema::{
30        CompilerRef,
31        hir::{Block, Contract, EventId, ItemId, Stmt, StmtKind as HirStmtKind},
32        ty::Gcx,
33    },
34};
35use std::{cell::OnceCell, fmt};
36use walkdir::WalkDir;
37
38/// The minimum Solidity version of the `Vm` interface.
39pub const MIN_VM_VERSION: Version = Version::new(0, 6, 2);
40
41/// Solidity source for the `Vm` interface in [forge-std](https://github.com/foundry-rs/forge-std)
42static VM_SOURCE: &str = include_str!("../../../testdata/utils/Vm.sol");
43
44/// [`SessionSource`] build output.
45pub struct GeneratedOutput {
46    output: ProjectCompileOutput,
47}
48
49impl fmt::Debug for GeneratedOutput {
50    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51        f.debug_struct("GeneratedOutput").finish_non_exhaustive()
52    }
53}
54
55impl GeneratedOutput {
56    /// Enters the solar compiler context, providing access to the HIR and `Gcx`.
57    pub fn enter<R: Send>(
58        &self,
59        f: impl for<'a, 'b, 'gcx> FnOnce(GeneratedOutputRef<'a, 'b, 'gcx>) -> R + Send,
60    ) -> R {
61        self.output
62            .parser()
63            .solc()
64            .compiler()
65            .enter(|c| f(GeneratedOutputRef { output: &self.output, compiler: c }))
66    }
67}
68
69/// A scoped reference to a [`GeneratedOutput`] together with an entered solar compiler.
70pub struct GeneratedOutputRef<'a, 'b, 'gcx> {
71    output: &'a ProjectCompileOutput,
72    pub(crate) compiler: &'b CompilerRef<'gcx>,
73}
74
75impl<'gcx> GeneratedOutputRef<'_, '_, 'gcx> {
76    pub fn gcx(&self) -> Gcx<'gcx> {
77        self.compiler.gcx()
78    }
79
80    pub fn repl_contract(&self) -> Option<&ConfigurableContractArtifact> {
81        self.output.find_first("REPL")
82    }
83
84    /// Looks up the REPL contract in the HIR.
85    pub fn repl_contract_hir(&self) -> Option<&'gcx Contract<'gcx>> {
86        self.gcx().hir.contracts().find(|c| c.name.as_str() == "REPL")
87    }
88
89    /// Returns the body block of the REPL `run()` function.
90    pub fn run_func_body(&self) -> Block<'gcx> {
91        let hir = &self.gcx().hir;
92        let c = self.repl_contract_hir().expect("REPL contract not found in HIR");
93        let f = c
94            .functions()
95            .find(|&f| {
96                let f = hir.function(f);
97                f.name.is_some_and(|n| n.as_str() == "run") && f.parameters.is_empty()
98            })
99            .expect("`run()` function not found in REPL contract");
100        hir.function(f).body.expect("`run()` function does not have a body")
101    }
102
103    /// Returns the [`EventId`] of an event named `input` in the REPL contract, if any.
104    pub fn get_event(&self, input: &str) -> Option<EventId> {
105        let hir = &self.gcx().hir;
106        let c = self.repl_contract_hir()?;
107        c.items.iter().find_map(|id| {
108            if let ItemId::Event(eid) = id
109                && hir.event(*eid).name.as_str() == input
110            {
111                Some(*eid)
112            } else {
113                None
114            }
115        })
116    }
117
118    pub fn final_pc(&self, contract: &ConfigurableContractArtifact) -> Result<Option<usize>> {
119        let deployed_bytecode = contract
120            .get_deployed_bytecode()
121            .ok_or_else(|| eyre::eyre!("No deployed bytecode found for `REPL` contract"))?;
122        let deployed_bytecode_bytes = deployed_bytecode
123            .bytes()
124            .ok_or_else(|| eyre::eyre!("No deployed bytecode found for `REPL` contract"))?;
125
126        // Fetch the run function's body statement
127        let run_body = self.run_func_body();
128
129        // Record loc of first yul block return statement (if any).
130        // This is used to decide which is the final statement within the `run()` method.
131        // see <https://github.com/foundry-rs/foundry/issues/4617>.
132        //
133        // Walk the AST of the REPL source to find a top-level `return(...)` call
134        // inside any `assembly { ... }` block in `run()`. This lets us pick the
135        // meaningful Yul return span even when HIR represents the block coarsely.
136        let last_yul_return_span: Option<Span> = self.first_yul_return_span();
137
138        // Find the last statement within the "run()" method and get the program
139        // counter via the source map.
140        let Some(last_stmt) = run_body.last() else { return Ok(None) };
141
142        // If the final statement is some type of block (unchecked or regular),
143        // we need to find the final statement within that block. Otherwise, default to
144        // the source loc of the final statement of the `run()` function's block.
145        //
146        // Inline assembly blocks are handled separately via
147        // `trailing_assembly_last_stmt_span`, which walks the AST to recover the last
148        // meaningful Yul statement.
149        let source_stmt = match &last_stmt.kind {
150            HirStmtKind::UncheckedBlock(stmts) | HirStmtKind::Block(stmts) => {
151                if let Some(stmt) = stmts.last() {
152                    stmt
153                } else {
154                    // In the case where the block is empty, attempt to grab the statement
155                    // before the block. Because we use saturating sub to get the second to
156                    // last index, this can always be safely unwrapped.
157                    &run_body[run_body.len().saturating_sub(2)]
158                }
159            }
160            _ => last_stmt,
161        };
162        // If the trailing statement is an assembly block, prefer the last meaningful
163        // (non-`let`) Yul statement's span as the source location for `final_pc`.
164        // See <https://github.com/foundry-rs/foundry/issues/4938>.
165        //
166        // `trailing_assembly_last_stmt_span` verifies via the AST that the HIR node
167        // corresponds to an assembly block and supplies the concrete Yul span to use.
168        let mut source_span =
169            if matches!(last_stmt.kind, HirStmtKind::AssemblyBlock(_) | HirStmtKind::Err(_))
170                && let Some(span) = self.trailing_assembly_last_stmt_span()
171            {
172                span
173            } else {
174                self.stmt_span_without_semicolon(source_stmt)
175            };
176
177        // Consider yul return statement as final statement (if it's loc is lower).
178        if let Some(yul_return_span) = last_yul_return_span
179            && yul_return_span.hi() < source_span.lo()
180        {
181            source_span = yul_return_span;
182        }
183
184        // Map the source location of the final statement of the `run()` function to its
185        // corresponding runtime program counter
186        let result = self
187            .compiler
188            .sess()
189            .source_map()
190            .span_to_source(source_span)
191            .map_err(|e| eyre::eyre!("failed to resolve span: {e:?}"))?;
192        let range = result.data;
193        let offset = range.start as u32;
194        let length = range.len() as u32;
195        trace!(%offset, %length, "find pc");
196        let final_pc = contract
197            .get_source_map_deployed()
198            .ok_or_else(|| eyre::eyre!("No source map found for `REPL` contract"))??
199            .into_iter()
200            .zip(InstIter::new(deployed_bytecode_bytes).with_pc().map(|(pc, _)| pc))
201            .filter(|(s, _)| s.offset() == offset && s.length() == length)
202            .map(|(_, pc)| pc)
203            .max();
204        trace!(?final_pc);
205        Ok(final_pc)
206    }
207
208    /// Statements' ranges in the solc source map do not include the semicolon.
209    fn stmt_span_without_semicolon(&self, stmt: &Stmt<'_>) -> Span {
210        match stmt.kind {
211            HirStmtKind::DeclSingle(id) => {
212                let decl = self.gcx().hir.variable(id);
213                if let Some(expr) = decl.initializer {
214                    stmt.span.with_hi(expr.span.hi())
215                } else {
216                    stmt.span
217                }
218            }
219            HirStmtKind::DeclMulti(_, expr) => stmt.span.with_hi(expr.span.hi()),
220            HirStmtKind::Expr(expr) => expr.span,
221            _ => stmt.span,
222        }
223    }
224
225    /// Returns the AST `run()` body of the REPL contract, if any.
226    ///
227    /// Returns the AST `run()` body so inline assembly blocks can be inspected at
228    /// Yul-statement granularity.
229    fn repl_run_ast_body(&self) -> Option<&'gcx solar::ast::Block<'gcx>> {
230        let contract = self.repl_contract_hir()?;
231        let source = self.gcx().sources.get(contract.source)?;
232        let ast = source.ast.as_ref()?;
233
234        let contract_ast = ast.items.iter().find_map(|i| match &i.kind {
235            ItemKind::Contract(c) if c.name.as_str() == "REPL" => Some(c),
236            _ => None,
237        })?;
238        contract_ast.body.iter().find_map(|i| match &i.kind {
239            ItemKind::Function(f)
240                if f.header.name.is_some_and(|n| n.as_str() == "run")
241                    && f.header.parameters.is_empty() =>
242            {
243                f.body.as_ref()
244            }
245            _ => None,
246        })
247    }
248
249    /// Returns the span of the first top-level `return(...)` call inside any
250    /// `assembly { ... }` block in the REPL `run()` function, if any.
251    fn first_yul_return_span(&self) -> Option<Span> {
252        let run_body = self.repl_run_ast_body()?;
253        for stmt in run_body.stmts.iter() {
254            let AstStmtKind::Assembly(asm) = &stmt.kind else { continue };
255            for ystmt in asm.block.stmts.iter() {
256                if let yul::StmtKind::Expr(e) = &ystmt.kind
257                    && let yul::ExprKind::Call(call) = &e.kind
258                    && call.name.as_str() == "return"
259                {
260                    return Some(ystmt.span);
261                }
262            }
263        }
264        None
265    }
266
267    /// If the last statement of the REPL `run()` function is an `assembly { ... }` block,
268    /// returns the span of its last non-`let` (i.e. non-VarDecl) Yul statement.
269    ///
270    /// This mirrors the legacy behavior used to pick a meaningful end-of-function PC when
271    /// the trailing statement is inline assembly.
272    fn trailing_assembly_last_stmt_span(&self) -> Option<Span> {
273        let run_body = self.repl_run_ast_body()?;
274        let AstStmtKind::Assembly(asm) = &run_body.stmts.last()?.kind else { return None };
275        asm.block
276            .stmts
277            .iter()
278            .rev()
279            .find(|s| !matches!(s.kind, yul::StmtKind::VarDecl(_, _)))
280            .map(|s| s.span)
281    }
282}
283
284/// Configuration for the [SessionSource]
285///
286/// Serialization is derived, but credential-bearing fields are always written through sanitizing
287/// getters, so every serialization path omits RPC and explorer credentials.
288#[derive(Clone, Debug, Default, Serialize, Deserialize)]
289#[serde(remote = "Self", bound = "")]
290pub struct SessionSourceConfig<FEN: FoundryEvmNetwork> {
291    /// Foundry configuration
292    #[serde(getter = "Self::persisted_foundry_config")]
293    pub foundry_config: Config,
294    /// EVM Options
295    #[serde(getter = "Self::persisted_evm_opts")]
296    pub evm_opts: EvmOpts,
297    /// Executor tooling selected by the concrete network dispatch.
298    #[serde(skip)]
299    pub executor_builder: ExecutorBuilder<FEN>,
300    /// Network family to restore when leaving fork mode.
301    #[serde(default)]
302    pub local_networks: Option<NetworkConfigs>,
303    /// Chain ID to restore when leaving fork mode.
304    #[serde(default)]
305    pub local_chain_id: Option<u64>,
306    /// Whether the saved fork network was inferred from its endpoint.
307    #[serde(default)]
308    pub fork_network_is_inferred: bool,
309    /// Whether the saved chain ID was inferred from its endpoint.
310    #[serde(default)]
311    pub fork_chain_id_is_inferred: bool,
312    /// Exact network hardfork selected for the latest execution.
313    #[serde(skip)]
314    pub resolved_hardfork: Option<FoundryHardfork>,
315    /// Source chain used for trace decoding and external identifiers.
316    #[serde(skip)]
317    pub source_chain_id: Option<u64>,
318    /// Disable the default `Vm` import.
319    pub no_vm: bool,
320    /// Cached execution backend and its fork identity.
321    #[serde(skip)]
322    pub(crate) cached_backend: Option<Backend<FEN>>,
323    /// Optionally enable traces for the REPL contract execution
324    pub traces: bool,
325    /// Optionally set calldata for the REPL contract execution
326    pub calldata: Option<Vec<u8>>,
327    /// Enable viaIR with minimum optimization
328    ///
329    /// This can fix most of the "stack too deep" errors while resulting a
330    /// relatively accurate source map.
331    pub ir_minimum: bool,
332    /// Whether a cached session needs the current invocation's fork endpoint.
333    #[serde(default, getter = "Self::persisted_fork_url_required")]
334    pub(crate) fork_url_required: bool,
335}
336
337impl<FEN: FoundryEvmNetwork> SessionSourceConfig<FEN> {
338    /// Captures the local execution context for sessions saved before it was persisted explicitly.
339    pub fn initialize_local_context(&mut self) {
340        self.evm_opts.fork_network_is_inferred = self.fork_network_is_inferred;
341        self.evm_opts.fork_chain_id_is_inferred = self.fork_chain_id_is_inferred;
342        if self.local_networks.is_none() {
343            self.local_networks = Some(self.evm_opts.networks);
344            self.local_chain_id =
345                self.evm_opts.env.chain_id.or(self.foundry_config.chain.map(|chain| chain.id()));
346        }
347    }
348
349    /// Detect the solc version to know if VM can be injected.
350    pub fn detect_solc(&mut self) -> Result<()> {
351        if self.foundry_config.solc.is_none() {
352            let version = Solc::ensure_installed(&"*".parse().unwrap())?;
353            self.foundry_config.solc = Some(SolcReq::Version(version));
354        }
355        if !self.no_vm
356            && let Some(version) = self.foundry_config.solc_version()
357            && version < MIN_VM_VERSION
358        {
359            info!(%version, minimum=%MIN_VM_VERSION, "Disabling VM injection");
360            self.no_vm = true;
361        }
362        Ok(())
363    }
364
365    /// Removes credentials from legacy caches while retaining whether the session was forked.
366    pub(crate) fn clear_credentials(&mut self) {
367        self.fork_url_required |= self.evm_opts.fork_url.is_some();
368        let none = InvocationRpc::default();
369        none.apply_config_credentials(&mut self.foundry_config);
370        none.apply_evm_credentials(&mut self.evm_opts);
371    }
372
373    /// Uses credentials from this invocation, never values persisted by an older Chisel version.
374    pub(crate) fn restore_credentials(&mut self, current: &InvocationRpc) -> Result<()> {
375        let forked = self.persisted_fork_url_required();
376        if forked && current.fork_url.is_none() {
377            eyre::bail!(
378                "this saved Chisel session requires a fork endpoint; use !fork <url> or restart Chisel with --fork-url to load it"
379            );
380        }
381        current.apply_config_credentials(&mut self.foundry_config);
382        current.apply_evm_credentials(&mut self.evm_opts);
383        current.apply_transport(&mut self.foundry_config, &mut self.evm_opts);
384        if !forked {
385            self.evm_opts.fork_url = None;
386        }
387        self.fork_url_required = false;
388        self.evm_opts.fork_endpoint = None;
389        self.evm_opts.expected_fork_endpoint = None;
390        self.resolved_hardfork = None;
391        self.source_chain_id = None;
392        self.cached_backend = None;
393        Ok(())
394    }
395
396    fn persisted_foundry_config(&self) -> Config {
397        let mut config = self.foundry_config.clone();
398        InvocationRpc::default().apply_config_credentials(&mut config);
399        config
400    }
401
402    fn persisted_evm_opts(&self) -> EvmOpts {
403        let mut evm_opts = self.evm_opts.clone();
404        InvocationRpc::default().apply_evm_credentials(&mut evm_opts);
405        evm_opts
406    }
407
408    /// Records that the session was forked even though its endpoint is not persisted.
409    const fn persisted_fork_url_required(&self) -> bool {
410        self.fork_url_required || self.evm_opts.fork_url.is_some()
411    }
412}
413
414impl<FEN: FoundryEvmNetwork> Serialize for SessionSourceConfig<FEN> {
415    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
416        Self::serialize(self, serializer)
417    }
418}
419
420impl<'de, FEN: FoundryEvmNetwork> Deserialize<'de> for SessionSourceConfig<FEN> {
421    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
422        Self::deserialize(deserializer)
423    }
424}
425
426/// RPC and explorer settings owned by the current Chisel invocation.
427///
428/// Loading a session restores credentials and transport settings from this invocation.
429/// The default value holds no credentials and is used to strip them from persisted sessions.
430#[derive(Clone, Debug, Default)]
431pub(crate) struct InvocationRpc {
432    eth_rpc_url: Option<String>,
433    eth_rpc_jwt: Option<String>,
434    eth_rpc_headers: Option<Vec<String>>,
435    etherscan_api_key: Option<String>,
436    etherscan: EtherscanConfigs,
437    rpc_endpoints: RpcEndpoints,
438    cached_endpoints: CachedEndpoints,
439    eth_rpc_timeout: Option<u64>,
440    eth_rpc_accept_invalid_certs: bool,
441    eth_rpc_no_proxy: bool,
442    fork_url: Option<String>,
443    fork_headers: Option<Vec<String>>,
444    rpc_jwt: Option<String>,
445    rpc_headers: Option<Vec<String>>,
446    rpc_timeout: Option<u64>,
447    rpc_accept_invalid_certs: bool,
448    rpc_no_proxy: bool,
449}
450
451impl InvocationRpc {
452    /// Captures the RPC settings resolved for this invocation.
453    pub(crate) fn capture(config: &Config, evm_opts: &EvmOpts) -> Self {
454        Self {
455            eth_rpc_url: config.eth_rpc_url.clone(),
456            eth_rpc_jwt: config.eth_rpc_jwt.clone(),
457            eth_rpc_headers: config.eth_rpc_headers.clone(),
458            etherscan_api_key: config.etherscan_api_key.clone(),
459            etherscan: config.etherscan.clone(),
460            rpc_endpoints: config.rpc_endpoints.clone(),
461            cached_endpoints: config.rpc_storage_caching.endpoints.clone(),
462            eth_rpc_timeout: config.eth_rpc_timeout,
463            eth_rpc_accept_invalid_certs: config.eth_rpc_accept_invalid_certs,
464            eth_rpc_no_proxy: config.eth_rpc_no_proxy,
465            fork_url: evm_opts.fork_url.clone(),
466            fork_headers: evm_opts.fork_headers.clone(),
467            rpc_jwt: evm_opts.rpc_jwt.clone(),
468            rpc_headers: evm_opts.rpc_headers.clone(),
469            rpc_timeout: evm_opts.rpc_timeout,
470            rpc_accept_invalid_certs: evm_opts.rpc_accept_invalid_certs,
471            rpc_no_proxy: evm_opts.rpc_no_proxy,
472        }
473    }
474
475    /// Replaces the fork endpoint and its endpoint-specific headers after `!fork <url>`.
476    pub(crate) fn set_fork(&mut self, url: String, headers: Option<Vec<String>>) {
477        self.eth_rpc_url = Some(url.clone());
478        self.fork_url = Some(url);
479        self.fork_headers = headers;
480    }
481
482    fn apply_config_credentials(&self, config: &mut Config) {
483        config.eth_rpc_url.clone_from(&self.eth_rpc_url);
484        config.eth_rpc_jwt.clone_from(&self.eth_rpc_jwt);
485        config.eth_rpc_headers.clone_from(&self.eth_rpc_headers);
486        config.etherscan_api_key.clone_from(&self.etherscan_api_key);
487        config.etherscan.clone_from(&self.etherscan);
488        config.rpc_endpoints.clone_from(&self.rpc_endpoints);
489        config.rpc_storage_caching.endpoints.clone_from(&self.cached_endpoints);
490    }
491
492    fn apply_evm_credentials(&self, evm_opts: &mut EvmOpts) {
493        evm_opts.fork_url.clone_from(&self.fork_url);
494        evm_opts.fork_headers.clone_from(&self.fork_headers);
495        evm_opts.rpc_jwt.clone_from(&self.rpc_jwt);
496        evm_opts.rpc_headers.clone_from(&self.rpc_headers);
497    }
498
499    const fn apply_transport(&self, config: &mut Config, evm_opts: &mut EvmOpts) {
500        config.eth_rpc_timeout = self.eth_rpc_timeout;
501        config.eth_rpc_accept_invalid_certs = self.eth_rpc_accept_invalid_certs;
502        config.eth_rpc_no_proxy = self.eth_rpc_no_proxy;
503        evm_opts.rpc_timeout = self.rpc_timeout;
504        evm_opts.rpc_accept_invalid_certs = self.rpc_accept_invalid_certs;
505        evm_opts.rpc_no_proxy = self.rpc_no_proxy;
506    }
507}
508
509/// REPL Session Source wrapper
510///
511/// Heavily based on soli's [`ConstructedSource`](https://github.com/jpopesculian/soli/blob/master/src/main.rs#L166)
512#[derive(Debug, Serialize, Deserialize)]
513#[serde(bound = "")]
514pub struct SessionSource<FEN: FoundryEvmNetwork> {
515    /// The file name
516    pub file_name: String,
517    /// The contract name
518    pub contract_name: String,
519
520    /// Session Source configuration
521    pub config: SessionSourceConfig<FEN>,
522
523    /// Global level Solidity code.
524    ///
525    /// Above and outside all contract declarations, in the global context.
526    pub global_code: String,
527    /// Top level Solidity code.
528    ///
529    /// Within the contract declaration, but outside of the `run()` function.
530    pub contract_code: String,
531    /// The code to be executed in the `run()` function.
532    pub run_code: String,
533
534    /// Cached VM source code.
535    #[serde(skip, default = "vm_source")]
536    vm_source: Source,
537    /// The generated output
538    #[serde(skip)]
539    output: OnceCell<GeneratedOutput>,
540}
541
542fn vm_source() -> Source {
543    Source::new(VM_SOURCE)
544}
545
546impl<FEN: FoundryEvmNetwork> Clone for SessionSource<FEN> {
547    fn clone(&self) -> Self {
548        Self {
549            file_name: self.file_name.clone(),
550            contract_name: self.contract_name.clone(),
551            global_code: self.global_code.clone(),
552            contract_code: self.contract_code.clone(),
553            run_code: self.run_code.clone(),
554            config: self.config.clone(),
555            vm_source: self.vm_source.clone(),
556            output: Default::default(),
557        }
558    }
559}
560
561impl<FEN: FoundryEvmNetwork> SessionSource<FEN> {
562    /// Creates a new source given a solidity compiler version
563    ///
564    /// # Panics
565    ///
566    /// If no Solc binary is set, cannot be found or the `--version` command fails
567    ///
568    /// ### Takes
569    ///
570    /// - An instance of [Solc]
571    /// - An instance of [SessionSourceConfig]
572    ///
573    /// ### Returns
574    ///
575    /// A new instance of [SessionSource]
576    pub fn new(mut config: SessionSourceConfig<FEN>) -> Result<Self> {
577        config.detect_solc()?;
578        Ok(Self {
579            file_name: "ReplContract.sol".to_string(),
580            contract_name: "REPL".to_string(),
581            config,
582            global_code: Default::default(),
583            contract_code: Default::default(),
584            run_code: Default::default(),
585            vm_source: vm_source(),
586            output: Default::default(),
587        })
588    }
589
590    /// Clones the [SessionSource] and appends a new line of code.
591    ///
592    /// Returns `true` if the new line was added to `run()`.
593    pub fn clone_with_new_line(&self, mut content: String) -> Result<(Self, bool)> {
594        if let Some((new_source, fragment)) = self
595            .parse_fragment(&content)
596            .or_else(|| {
597                content.push(';');
598                self.parse_fragment(&content)
599            })
600            .or_else(|| {
601                content = content.trim_end().trim_end_matches(';').to_string();
602                self.parse_fragment(&content)
603            })
604        {
605            Ok((new_source, matches!(fragment, ParseTreeFragment::Function)))
606        } else {
607            eyre::bail!("\"{}\"", content.trim());
608        }
609    }
610
611    /// Parses a fragment of Solidity code in memory and assigns it a scope within the
612    /// [`SessionSource`].
613    fn parse_fragment(&self, buffer: &str) -> Option<(Self, ParseTreeFragment)> {
614        #[track_caller]
615        fn debug_errors(errors: &EmittedDiagnostics) {
616            debug!("{errors}");
617        }
618
619        let mut this = self.clone();
620        match this.add_run_code(buffer).parse() {
621            Ok(()) => return Some((this, ParseTreeFragment::Function)),
622            Err(e) => debug_errors(&e),
623        }
624        this = self.clone();
625        match this.add_contract_code(buffer).parse() {
626            Ok(()) => return Some((this, ParseTreeFragment::Contract)),
627            Err(e) => debug_errors(&e),
628        }
629        this = self.clone();
630        match this.add_global_code(buffer).parse() {
631            Ok(()) => return Some((this, ParseTreeFragment::Source)),
632            Err(e) => debug_errors(&e),
633        }
634        None
635    }
636
637    /// Append global-level code to the source.
638    pub fn add_global_code(&mut self, content: &str) -> &mut Self {
639        self.global_code.push_str(content.trim());
640        self.global_code.push('\n');
641        self.clear_output();
642        self
643    }
644
645    /// Append contract-level code to the source.
646    pub fn add_contract_code(&mut self, content: &str) -> &mut Self {
647        self.contract_code.push_str(content.trim());
648        self.contract_code.push('\n');
649        self.clear_output();
650        self
651    }
652
653    /// Append code to the `run()` function of the REPL contract.
654    pub fn add_run_code(&mut self, content: &str) -> &mut Self {
655        self.run_code.push_str(content.trim());
656        self.run_code.push('\n');
657        self.clear_output();
658        self
659    }
660
661    /// Clears all source code.
662    pub fn clear(&mut self) {
663        String::clear(&mut self.global_code);
664        String::clear(&mut self.contract_code);
665        String::clear(&mut self.run_code);
666        self.clear_output();
667    }
668
669    /// Clear the `run()` function code.
670    pub fn clear_run(&mut self) -> &mut Self {
671        String::clear(&mut self.run_code);
672        self.clear_output();
673        self
674    }
675
676    fn clear_output(&mut self) {
677        self.output.take();
678    }
679
680    /// Compiles the source if necessary.
681    pub fn build(&self) -> Result<&GeneratedOutput> {
682        // TODO: mimics `get_or_try_init`
683        if let Some(output) = self.output.get() {
684            return Ok(output);
685        }
686        let output = self.compile()?;
687        let output = GeneratedOutput { output };
688        Ok(self.output.get_or_init(|| output))
689    }
690
691    /// Compiles the source.
692    #[cold]
693    fn compile(&self) -> Result<ProjectCompileOutput> {
694        let sources = self.get_sources();
695
696        let mut project = self.config.foundry_config.ephemeral_project()?;
697        self.config.foundry_config.disable_optimizations(&mut project, self.config.ir_minimum);
698        let mut output = ProjectCompiler::with_sources(&project, sources)?.compile()?;
699
700        if output.has_compiler_errors() {
701            eyre::bail!("{output}");
702        }
703
704        // Drive HIR lowering and analysis so that subsequent `enter` queries can use them.
705        // Chisel inspects expression values, so enable Solar's expression type table.
706        let compiler = output.parser_mut().solc_mut().compiler_mut();
707        compiler.enter_mut(|c| {
708            let _ = c.lower_asts();
709            let _ = c.analysis();
710        });
711
712        Ok(output)
713    }
714
715    fn get_sources(&self) -> Sources {
716        let mut sources = Sources::new();
717
718        let src = self.to_repl_source();
719        sources.insert(self.file_name.clone().into(), Source::new(src));
720
721        // Include Vm.sol if forge-std remapping is not available.
722        if !self.config.no_vm
723            && !self
724                .config
725                .foundry_config
726                .get_all_remappings()
727                .any(|r| r.name.starts_with("forge-std"))
728        {
729            sources.insert("forge-std/Vm.sol".into(), self.vm_source.clone());
730        }
731
732        sources
733    }
734
735    /// Construct the REPL source.
736    pub fn to_repl_source(&self) -> String {
737        let Self {
738            contract_name,
739            global_code,
740            contract_code: top_level_code,
741            run_code,
742            config,
743            ..
744        } = self;
745        let (mut vm_import, mut vm_constant) = (String::new(), String::new());
746        // Check if there's any `forge-std` remapping and determine proper path to it by
747        // searching remapping path.
748        if !config.no_vm
749            && let Some(remapping) = config
750                .foundry_config
751                .remappings
752                .iter()
753                .find(|remapping| remapping.name == "forge-std/")
754            && let Some(vm_path) = WalkDir::new(&remapping.path.path)
755                .into_iter()
756                .filter_map(|e| e.ok())
757                .find(|e| e.file_name() == "Vm.sol")
758        {
759            vm_import = format!(
760                "import {{Vm}} from \"{}\";\n",
761                vm_path.path().to_string_lossy().replace('\\', "/")
762            );
763            vm_constant = "Vm internal constant vm = Vm(address(uint160(uint256(keccak256(\"hevm cheat code\")))));\n".to_string();
764        }
765
766        format!(
767            r#"
768// SPDX-License-Identifier: UNLICENSED
769pragma solidity 0;
770
771{vm_import}
772{global_code}
773
774contract {contract_name} {{
775    {vm_constant}
776    {top_level_code}
777
778    /// @notice REPL contract entry point
779    function run() public {{
780        {run_code}
781    }}
782}}"#,
783        )
784    }
785
786    /// Parse the current source in memory using Solar.
787    pub(crate) fn parse(&self) -> Result<(), EmittedDiagnostics> {
788        let sess =
789            solar::interface::Session::builder().with_buffer_emitter(Default::default()).build();
790        let _ = sess.enter_sequential(|| -> solar::interface::Result<()> {
791            let arena = solar::ast::Arena::new();
792            let filename = self.file_name.clone().into();
793            let src = self.to_repl_source();
794            let mut parser = solar::parse::Parser::from_source_code(&sess, &arena, filename, src)?;
795            let _ast = parser.parse_file().map_err(|e| e.emit())?;
796            Ok(())
797        });
798        sess.dcx.emitted_errors().unwrap()
799    }
800}
801
802/// A Parse Tree Fragment
803///
804/// Used to determine whether an input will go to the "run()" function,
805/// the top level of the contract, or in global scope.
806#[derive(Debug)]
807enum ParseTreeFragment {
808    /// Code for the global scope
809    Source,
810    /// Code for the top level of the contract
811    Contract,
812    /// Code for the "run()" function
813    Function,
814}
815
816#[cfg(test)]
817mod tests {
818    use super::*;
819    use foundry_compilers::artifacts::remappings::{RelativeRemapping, RelativeRemappingPathBuf};
820    use foundry_evm::core::evm::EthEvmNetwork;
821    use std::fs;
822
823    #[test]
824    fn initialize_local_context_migrates_legacy_session() {
825        let mut config = SessionSourceConfig::<EthEvmNetwork>::default();
826        config.evm_opts.networks = NetworkConfigs::with_tempo();
827        config.evm_opts.env.chain_id = Some(4217);
828
829        config.initialize_local_context();
830
831        assert_eq!(config.local_networks, Some(NetworkConfigs::with_tempo()));
832        assert_eq!(config.local_chain_id, Some(4217));
833
834        config.evm_opts.networks = NetworkConfigs::default();
835        config.evm_opts.env.chain_id = Some(1);
836        config.initialize_local_context();
837
838        assert_eq!(config.local_networks, Some(NetworkConfigs::with_tempo()));
839        assert_eq!(config.local_chain_id, Some(4217));
840    }
841
842    #[test]
843    fn serialized_session_restores_fork_inference_provenance() {
844        let config = SessionSourceConfig::<EthEvmNetwork> {
845            fork_network_is_inferred: true,
846            fork_chain_id_is_inferred: true,
847            ..Default::default()
848        };
849        let encoded = serde_json::to_string(&config).unwrap();
850        let mut decoded =
851            serde_json::from_str::<SessionSourceConfig<EthEvmNetwork>>(&encoded).unwrap();
852
853        assert!(!decoded.evm_opts.fork_network_is_inferred);
854        assert!(!decoded.evm_opts.fork_chain_id_is_inferred);
855        decoded.initialize_local_context();
856        assert!(decoded.evm_opts.fork_network_is_inferred);
857        assert!(decoded.evm_opts.fork_chain_id_is_inferred);
858    }
859
860    #[test]
861    fn legacy_session_without_rpc_transport_flags_deserializes() {
862        let config = SessionSourceConfig::<EthEvmNetwork>::default();
863        let mut legacy_session = serde_json::to_value(config).unwrap();
864        let evm_opts = legacy_session["evm_opts"].as_object_mut().expect("serialized EVM options");
865        assert!(evm_opts.remove("eth_rpc_accept_invalid_certs").is_some());
866        assert!(evm_opts.remove("eth_rpc_no_proxy").is_some());
867
868        let decoded =
869            serde_json::from_value::<SessionSourceConfig<EthEvmNetwork>>(legacy_session).unwrap();
870
871        assert!(!decoded.evm_opts.rpc_accept_invalid_certs);
872        assert!(!decoded.evm_opts.rpc_no_proxy);
873    }
874
875    /// Regression test for <https://github.com/foundry-rs/foundry/issues/14711>.
876    ///
877    /// `to_repl_source()` must use forward slashes in the Vm import path regardless of OS,
878    /// because Solidity import statements require `/` as the path separator.
879    #[test]
880    fn test_vm_import_path_uses_forward_slashes() {
881        let tmp = tempfile::tempdir().unwrap();
882        let vm_sol = tmp.path().join("Vm.sol");
883        fs::write(&vm_sol, "// dummy").unwrap();
884
885        let remapping = RelativeRemapping {
886            context: None,
887            name: "forge-std/".to_string(),
888            path: RelativeRemappingPathBuf { parent: None, path: tmp.path().to_path_buf() },
889        };
890
891        let mut config: SessionSourceConfig<EthEvmNetwork> = SessionSourceConfig {
892            foundry_config: Config {
893                solc: Some(SolcReq::Version(Version::new(0, 8, 29))),
894                remappings: vec![remapping],
895                ..Default::default()
896            },
897            ..Default::default()
898        };
899        // Pre-set solc so detect_solc() skips the ensure_installed I/O.
900        config.detect_solc().unwrap();
901
902        let source = SessionSource {
903            file_name: "ReplContract.sol".to_string(),
904            contract_name: "REPL".to_string(),
905            config,
906            global_code: Default::default(),
907            contract_code: Default::default(),
908            run_code: Default::default(),
909            vm_source: vm_source(),
910            output: Default::default(),
911        };
912
913        let repl = source.to_repl_source();
914        let import_line = repl.lines().find(|l| l.contains("import {Vm}")).unwrap();
915        assert!(
916            !import_line.contains('\\'),
917            "Vm import path must not contain backslashes, got: {import_line}"
918        );
919        assert!(import_line.contains('/'), "Vm import path must use forward slashes");
920    }
921
922    #[test]
923    fn session_serialization_omits_credentials() {
924        let mut config = SessionSourceConfig::<EthEvmNetwork>::default();
925        config.foundry_config.eth_rpc_url =
926            Some("https://user:synthetic-password@rpc.invalid/key".into());
927        config.foundry_config.eth_rpc_jwt = Some("synthetic-jwt".into());
928        config.foundry_config.eth_rpc_headers =
929            Some(vec!["Authorization: synthetic-header".into()]);
930        config.foundry_config.etherscan_api_key = Some("synthetic-api-key".into());
931        config.foundry_config.etherscan = serde_json::from_value(serde_json::json!({
932            "mainnet": { "key": "synthetic-explorer-key", "chain": 1 }
933        }))
934        .unwrap();
935        config.foundry_config.rpc_endpoints = serde_json::from_value(serde_json::json!({
936            "mainnet": "https://rpc.invalid/synthetic-endpoint-key"
937        }))
938        .unwrap();
939        config.foundry_config.rpc_storage_caching.endpoints =
940            "synthetic-cache-endpoint-key".parse().unwrap();
941        config.evm_opts.fork_url = config.foundry_config.eth_rpc_url.clone();
942        config.evm_opts.rpc_jwt = config.foundry_config.eth_rpc_jwt.clone();
943        config.evm_opts.rpc_headers = config.foundry_config.eth_rpc_headers.clone();
944        config.evm_opts.fork_headers = Some(vec!["Authorization: synthetic-fork-header".into()]);
945        config.evm_opts.fork_block_number = Some(42);
946        config.calldata = Some(vec![0xde, 0xad, 0xbe, 0xef]);
947        config.traces = true;
948
949        let encoded = serde_json::to_string(&config).unwrap();
950        let decoded: SessionSourceConfig<EthEvmNetwork> = serde_json::from_str(&encoded).unwrap();
951
952        assert_eq!(decoded.foundry_config.eth_rpc_url, None);
953        assert_eq!(decoded.foundry_config.eth_rpc_jwt, None);
954        assert_eq!(decoded.foundry_config.eth_rpc_headers, None);
955        assert_eq!(decoded.foundry_config.etherscan_api_key, None);
956        assert!(decoded.foundry_config.etherscan.is_empty());
957        assert!(decoded.foundry_config.rpc_endpoints.is_empty());
958        assert_eq!(decoded.foundry_config.rpc_storage_caching.endpoints.to_string(), "all");
959        assert_eq!(decoded.evm_opts.fork_url, None);
960        assert_eq!(decoded.evm_opts.rpc_jwt, None);
961        assert_eq!(decoded.evm_opts.rpc_headers, None);
962        assert_eq!(decoded.evm_opts.fork_headers, None);
963        assert_eq!(decoded.evm_opts.fork_block_number, Some(42));
964        assert_eq!(decoded.calldata, Some(vec![0xde, 0xad, 0xbe, 0xef]));
965        assert!(decoded.traces);
966        assert!(decoded.fork_url_required);
967        assert_eq!(config.foundry_config.eth_rpc_jwt.as_deref(), Some("synthetic-jwt"));
968        assert_eq!(config.evm_opts.fork_url, config.foundry_config.eth_rpc_url);
969    }
970
971    /// Pins the persisted schema so that a new field is a deliberate decision to cache or skip it.
972    #[test]
973    fn session_serialization_round_trips_persisted_fields() {
974        let config = SessionSourceConfig::<EthEvmNetwork> {
975            foundry_config: Config { optimizer_runs: Some(500), ..Default::default() },
976            evm_opts: EvmOpts { fork_block_number: Some(42), ..Default::default() },
977            local_networks: Some(NetworkConfigs::with_tempo()),
978            local_chain_id: Some(4217),
979            fork_network_is_inferred: true,
980            fork_chain_id_is_inferred: true,
981            no_vm: true,
982            traces: true,
983            calldata: Some(vec![1, 2, 3]),
984            ir_minimum: true,
985            fork_url_required: true,
986            source_chain_id: Some(1),
987            ..Default::default()
988        };
989
990        let encoded = serde_json::to_value(&config).unwrap();
991        let mut keys = encoded.as_object().unwrap().keys().map(String::as_str).collect::<Vec<_>>();
992        keys.sort_unstable();
993        assert_eq!(
994            keys,
995            [
996                "calldata",
997                "evm_opts",
998                "fork_chain_id_is_inferred",
999                "fork_network_is_inferred",
1000                "fork_url_required",
1001                "foundry_config",
1002                "ir_minimum",
1003                "local_chain_id",
1004                "local_networks",
1005                "no_vm",
1006                "traces",
1007            ]
1008        );
1009
1010        let decoded =
1011            serde_json::from_value::<SessionSourceConfig<EthEvmNetwork>>(encoded).unwrap();
1012        assert_eq!(decoded.foundry_config.optimizer_runs, Some(500));
1013        assert_eq!(decoded.evm_opts.fork_block_number, Some(42));
1014        assert!(decoded.local_networks.is_some_and(|networks| networks.is_tempo()));
1015        assert_eq!(decoded.local_chain_id, Some(4217));
1016        assert!(decoded.fork_network_is_inferred);
1017        assert!(decoded.fork_chain_id_is_inferred);
1018        assert!(decoded.no_vm);
1019        assert!(decoded.traces);
1020        assert_eq!(decoded.calldata, Some(vec![1, 2, 3]));
1021        assert!(decoded.ir_minimum);
1022        assert!(decoded.fork_url_required);
1023        assert_eq!(decoded.source_chain_id, None);
1024    }
1025
1026    #[test]
1027    fn saved_fork_restores_current_credentials_and_execution_settings() {
1028        let mut saved = SessionSourceConfig::<EthEvmNetwork>::default();
1029        saved.evm_opts.fork_url = Some("https://rpc.invalid/old-token".into());
1030        saved.evm_opts.fork_block_number = Some(42);
1031        saved.evm_opts.env.chain_id = Some(1);
1032        saved.foundry_config.optimizer_runs = Some(500);
1033        saved.foundry_config.eth_rpc_accept_invalid_certs = true;
1034        saved.foundry_config.eth_rpc_no_proxy = true;
1035        saved.evm_opts.rpc_accept_invalid_certs = true;
1036        saved.evm_opts.rpc_no_proxy = true;
1037        saved.calldata = Some(vec![1, 2, 3]);
1038        saved.ir_minimum = true;
1039        let encoded = serde_json::to_string(&saved).unwrap();
1040        let mut saved =
1041            serde_json::from_str::<SessionSourceConfig<EthEvmNetwork>>(&encoded).unwrap();
1042        let mut current = SessionSourceConfig::<EthEvmNetwork>::default();
1043        current.foundry_config.eth_rpc_url = Some("https://rpc.invalid/new-token".into());
1044        current.foundry_config.eth_rpc_jwt = Some("current-jwt".into());
1045        current.foundry_config.eth_rpc_headers = Some(vec!["Authorization: current-header".into()]);
1046        current.foundry_config.etherscan_api_key = Some("current-api-key".into());
1047        current.foundry_config.rpc_endpoints = serde_json::from_value(serde_json::json!({
1048            "mainnet": "https://rpc.invalid/current-endpoint-token"
1049        }))
1050        .unwrap();
1051        current.foundry_config.etherscan = serde_json::from_value(serde_json::json!({
1052            "mainnet": { "key": "current-explorer-key", "chain": 1 }
1053        }))
1054        .unwrap();
1055        current.foundry_config.rpc_storage_caching.endpoints =
1056            "current-cache-endpoint-token".parse().unwrap();
1057        current.evm_opts.fork_url = current.foundry_config.eth_rpc_url.clone();
1058        current.evm_opts.rpc_jwt = current.foundry_config.eth_rpc_jwt.clone();
1059        current.evm_opts.rpc_headers = current.foundry_config.eth_rpc_headers.clone();
1060        current.evm_opts.fork_headers = Some(vec!["Authorization: current-fork-header".into()]);
1061        current.evm_opts.fork_block_number = Some(100);
1062        current.evm_opts.env.chain_id = Some(10);
1063        current.foundry_config.eth_rpc_timeout = Some(30);
1064        current.evm_opts.rpc_timeout = Some(30);
1065
1066        saved
1067            .restore_credentials(&InvocationRpc::capture(
1068                &current.foundry_config,
1069                &current.evm_opts,
1070            ))
1071            .unwrap();
1072
1073        assert_eq!(saved.foundry_config.eth_rpc_url, current.foundry_config.eth_rpc_url);
1074        assert_eq!(saved.foundry_config.eth_rpc_jwt, current.foundry_config.eth_rpc_jwt);
1075        assert_eq!(saved.foundry_config.eth_rpc_headers, current.foundry_config.eth_rpc_headers);
1076        assert_eq!(
1077            saved.foundry_config.etherscan_api_key,
1078            current.foundry_config.etherscan_api_key
1079        );
1080        assert_eq!(saved.foundry_config.etherscan, current.foundry_config.etherscan);
1081        assert_eq!(saved.foundry_config.rpc_endpoints, current.foundry_config.rpc_endpoints);
1082        assert_eq!(
1083            saved.foundry_config.rpc_storage_caching.endpoints,
1084            current.foundry_config.rpc_storage_caching.endpoints
1085        );
1086        assert_eq!(saved.evm_opts.fork_url, current.evm_opts.fork_url);
1087        assert_eq!(saved.evm_opts.rpc_jwt, current.evm_opts.rpc_jwt);
1088        assert_eq!(saved.evm_opts.rpc_headers, current.evm_opts.rpc_headers);
1089        assert_eq!(saved.evm_opts.fork_headers, current.evm_opts.fork_headers);
1090        assert!(!saved.foundry_config.eth_rpc_accept_invalid_certs);
1091        assert!(!saved.foundry_config.eth_rpc_no_proxy);
1092        assert!(!saved.evm_opts.rpc_accept_invalid_certs);
1093        assert!(!saved.evm_opts.rpc_no_proxy);
1094        assert_eq!(saved.foundry_config.eth_rpc_timeout, Some(30));
1095        assert_eq!(saved.evm_opts.rpc_timeout, Some(30));
1096        assert_eq!(saved.evm_opts.fork_block_number, Some(42));
1097        assert_eq!(saved.evm_opts.env.chain_id, Some(1));
1098        assert_eq!(saved.foundry_config.optimizer_runs, Some(500));
1099        assert_eq!(saved.calldata, Some(vec![1, 2, 3]));
1100        assert!(saved.ir_minimum);
1101    }
1102
1103    #[test]
1104    fn legacy_session_credentials_are_not_reused() {
1105        let mut legacy = SessionSourceConfig::<EthEvmNetwork>::default();
1106        legacy.foundry_config.eth_rpc_jwt = Some("legacy-jwt".into());
1107        legacy.foundry_config.etherscan_api_key = Some("legacy-key".into());
1108        legacy.evm_opts.rpc_headers = Some(vec!["Authorization: legacy-header".into()]);
1109        legacy.evm_opts.fork_headers = Some(vec!["Authorization: legacy-fork-header".into()]);
1110        legacy.restore_credentials(&InvocationRpc::default()).unwrap();
1111
1112        assert_eq!(legacy.foundry_config.eth_rpc_jwt, None);
1113        assert_eq!(legacy.foundry_config.etherscan_api_key, None);
1114        assert_eq!(legacy.evm_opts.rpc_headers, None);
1115        assert_eq!(legacy.evm_opts.fork_headers, None);
1116    }
1117
1118    #[test]
1119    fn saved_fork_requires_current_endpoint() {
1120        let mut saved = SessionSourceConfig::<EthEvmNetwork>::default();
1121        saved.evm_opts.fork_url = Some("https://rpc.invalid/legacy-token".into());
1122        saved.clear_credentials();
1123        assert_eq!(saved.evm_opts.fork_url, None);
1124        assert!(saved.fork_url_required);
1125
1126        let error = saved.restore_credentials(&InvocationRpc::default()).unwrap_err();
1127
1128        assert_eq!(
1129            error.to_string(),
1130            "this saved Chisel session requires a fork endpoint; use !fork <url> or restart Chisel with --fork-url to load it"
1131        );
1132    }
1133
1134    #[test]
1135    fn saved_local_session_stays_local_with_current_fork_endpoint() {
1136        let mut saved = SessionSourceConfig::<EthEvmNetwork>::default();
1137        let mut current = SessionSourceConfig::<EthEvmNetwork>::default();
1138        current.evm_opts.fork_url = Some("https://rpc.invalid/current-token".into());
1139
1140        saved
1141            .restore_credentials(&InvocationRpc::capture(
1142                &current.foundry_config,
1143                &current.evm_opts,
1144            ))
1145            .unwrap();
1146
1147        assert_eq!(saved.evm_opts.fork_url, None);
1148        assert!(!saved.fork_url_required);
1149    }
1150}