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::{Config, FoundryHardfork, SolcReq};
15use foundry_evm::{
16    backend::Backend,
17    core::{bytecode::InstIter, evm::FoundryEvmNetwork},
18    fork::ResolvedFork,
19    opts::EvmOpts,
20};
21use foundry_evm_networks::NetworkConfigs;
22use semver::Version;
23use serde::{Deserialize, Serialize};
24use solar::{
25    ast::{ItemKind, StmtKind as AstStmtKind, yul},
26    interface::{Span, diagnostics::EmittedDiagnostics},
27    sema::{
28        CompilerRef,
29        hir::{Block, Contract, EventId, ItemId, Stmt, StmtKind as HirStmtKind},
30        ty::Gcx,
31    },
32};
33use std::{cell::OnceCell, fmt};
34use walkdir::WalkDir;
35
36/// The minimum Solidity version of the `Vm` interface.
37pub const MIN_VM_VERSION: Version = Version::new(0, 6, 2);
38
39/// Solidity source for the `Vm` interface in [forge-std](https://github.com/foundry-rs/forge-std)
40static VM_SOURCE: &str = include_str!("../../../testdata/utils/Vm.sol");
41
42/// In-memory backend and the exact fork identity from which it was constructed.
43#[derive(Clone, Debug)]
44pub(crate) struct CachedBackend<FEN: FoundryEvmNetwork> {
45    pub(crate) backend: Backend<FEN>,
46    pub(crate) resolved_fork: Option<ResolvedFork>,
47}
48
49/// [`SessionSource`] build output.
50pub struct GeneratedOutput {
51    output: ProjectCompileOutput,
52}
53
54impl fmt::Debug for GeneratedOutput {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        f.debug_struct("GeneratedOutput").finish_non_exhaustive()
57    }
58}
59
60impl GeneratedOutput {
61    /// Enters the solar compiler context, providing access to the HIR and `Gcx`.
62    pub fn enter<R: Send>(
63        &self,
64        f: impl for<'a, 'b, 'gcx> FnOnce(GeneratedOutputRef<'a, 'b, 'gcx>) -> R + Send,
65    ) -> R {
66        self.output
67            .parser()
68            .solc()
69            .compiler()
70            .enter(|c| f(GeneratedOutputRef { output: &self.output, compiler: c }))
71    }
72}
73
74/// A scoped reference to a [`GeneratedOutput`] together with an entered solar compiler.
75pub struct GeneratedOutputRef<'a, 'b, 'gcx> {
76    output: &'a ProjectCompileOutput,
77    pub(crate) compiler: &'b CompilerRef<'gcx>,
78}
79
80impl<'gcx> GeneratedOutputRef<'_, '_, 'gcx> {
81    pub fn gcx(&self) -> Gcx<'gcx> {
82        self.compiler.gcx()
83    }
84
85    pub fn repl_contract(&self) -> Option<&ConfigurableContractArtifact> {
86        self.output.find_first("REPL")
87    }
88
89    /// Looks up the REPL contract in the HIR.
90    pub fn repl_contract_hir(&self) -> Option<&'gcx Contract<'gcx>> {
91        self.gcx().hir.contracts().find(|c| c.name.as_str() == "REPL")
92    }
93
94    /// Returns the body block of the REPL `run()` function.
95    pub fn run_func_body(&self) -> Block<'gcx> {
96        let hir = &self.gcx().hir;
97        let c = self.repl_contract_hir().expect("REPL contract not found in HIR");
98        let f = c
99            .functions()
100            .find(|&f| hir.function(f).name.as_ref().map(|n| n.as_str()) == Some("run"))
101            .expect("`run()` function not found in REPL contract");
102        hir.function(f).body.expect("`run()` function does not have a body")
103    }
104
105    /// Returns the [`EventId`] of an event named `input` in the REPL contract, if any.
106    pub fn get_event(&self, input: &str) -> Option<EventId> {
107        let hir = &self.gcx().hir;
108        let c = self.repl_contract_hir()?;
109        c.items.iter().find_map(|id| {
110            if let ItemId::Event(eid) = id
111                && hir.event(*eid).name.as_str() == input
112            {
113                Some(*eid)
114            } else {
115                None
116            }
117        })
118    }
119
120    pub fn final_pc(&self, contract: &ConfigurableContractArtifact) -> Result<Option<usize>> {
121        let deployed_bytecode = contract
122            .get_deployed_bytecode()
123            .ok_or_else(|| eyre::eyre!("No deployed bytecode found for `REPL` contract"))?;
124        let deployed_bytecode_bytes = deployed_bytecode
125            .bytes()
126            .ok_or_else(|| eyre::eyre!("No deployed bytecode found for `REPL` contract"))?;
127
128        // Fetch the run function's body statement
129        let run_body = self.run_func_body();
130
131        // Record loc of first yul block return statement (if any).
132        // This is used to decide which is the final statement within the `run()` method.
133        // see <https://github.com/foundry-rs/foundry/issues/4617>.
134        //
135        // Walk the AST of the REPL source to find a top-level `return(...)` call
136        // inside any `assembly { ... }` block in `run()`. This lets us pick the
137        // meaningful Yul return span even when HIR represents the block coarsely.
138        let last_yul_return_span: Option<Span> = self.first_yul_return_span();
139
140        // Find the last statement within the "run()" method and get the program
141        // counter via the source map.
142        let Some(last_stmt) = run_body.last() else { return Ok(None) };
143
144        // If the final statement is some type of block (unchecked or regular),
145        // we need to find the final statement within that block. Otherwise, default to
146        // the source loc of the final statement of the `run()` function's block.
147        //
148        // Inline assembly blocks are handled separately via
149        // `trailing_assembly_last_stmt_span`, which walks the AST to recover the last
150        // meaningful Yul statement.
151        let source_stmt = match &last_stmt.kind {
152            HirStmtKind::UncheckedBlock(stmts) | HirStmtKind::Block(stmts) => {
153                if let Some(stmt) = stmts.last() {
154                    stmt
155                } else {
156                    // In the case where the block is empty, attempt to grab the statement
157                    // before the block. Because we use saturating sub to get the second to
158                    // last index, this can always be safely unwrapped.
159                    &run_body[run_body.len().saturating_sub(2)]
160                }
161            }
162            _ => last_stmt,
163        };
164        // If the trailing statement is an assembly block, prefer the last meaningful
165        // (non-`let`) Yul statement's span as the source location for `final_pc`.
166        // See <https://github.com/foundry-rs/foundry/issues/4938>.
167        //
168        // `trailing_assembly_last_stmt_span` verifies via the AST that the HIR node
169        // corresponds to an assembly block and supplies the concrete Yul span to use.
170        let mut source_span =
171            if matches!(last_stmt.kind, HirStmtKind::AssemblyBlock(_) | HirStmtKind::Err(_))
172                && let Some(span) = self.trailing_assembly_last_stmt_span()
173            {
174                span
175            } else {
176                self.stmt_span_without_semicolon(source_stmt)
177            };
178
179        // Consider yul return statement as final statement (if it's loc is lower).
180        if let Some(yul_return_span) = last_yul_return_span
181            && yul_return_span.hi() < source_span.lo()
182        {
183            source_span = yul_return_span;
184        }
185
186        // Map the source location of the final statement of the `run()` function to its
187        // corresponding runtime program counter
188        let result = self
189            .compiler
190            .sess()
191            .source_map()
192            .span_to_source(source_span)
193            .map_err(|e| eyre::eyre!("failed to resolve span: {e:?}"))?;
194        let range = result.data;
195        let offset = range.start as u32;
196        let length = range.len() as u32;
197        trace!(%offset, %length, "find pc");
198        let final_pc = contract
199            .get_source_map_deployed()
200            .ok_or_else(|| eyre::eyre!("No source map found for `REPL` contract"))??
201            .into_iter()
202            .zip(InstIter::new(deployed_bytecode_bytes).with_pc().map(|(pc, _)| pc))
203            .filter(|(s, _)| s.offset() == offset && s.length() == length)
204            .map(|(_, pc)| pc)
205            .max();
206        trace!(?final_pc);
207        Ok(final_pc)
208    }
209
210    /// Statements' ranges in the solc source map do not include the semicolon.
211    fn stmt_span_without_semicolon(&self, stmt: &Stmt<'_>) -> Span {
212        match stmt.kind {
213            HirStmtKind::DeclSingle(id) => {
214                let decl = self.gcx().hir.variable(id);
215                if let Some(expr) = decl.initializer {
216                    stmt.span.with_hi(expr.span.hi())
217                } else {
218                    stmt.span
219                }
220            }
221            HirStmtKind::DeclMulti(_, expr) => stmt.span.with_hi(expr.span.hi()),
222            HirStmtKind::Expr(expr) => expr.span,
223            _ => stmt.span,
224        }
225    }
226
227    /// Returns the AST `run()` body of the REPL contract, if any.
228    ///
229    /// Returns the AST `run()` body so inline assembly blocks can be inspected at
230    /// Yul-statement granularity.
231    fn repl_run_ast_body(&self) -> Option<&'gcx solar::ast::Block<'gcx>> {
232        let contract = self.repl_contract_hir()?;
233        let source = self.gcx().sources.get(contract.source)?;
234        let ast = source.ast.as_ref()?;
235
236        let contract_ast = ast.items.iter().find_map(|i| match &i.kind {
237            ItemKind::Contract(c) if c.name.as_str() == "REPL" => Some(c),
238            _ => None,
239        })?;
240        contract_ast.body.iter().find_map(|i| match &i.kind {
241            ItemKind::Function(f) if f.header.name.is_some_and(|n| n.as_str() == "run") => {
242                f.body.as_ref()
243            }
244            _ => None,
245        })
246    }
247
248    /// Returns the span of the first top-level `return(...)` call inside any
249    /// `assembly { ... }` block in the REPL `run()` function, if any.
250    fn first_yul_return_span(&self) -> Option<Span> {
251        let run_body = self.repl_run_ast_body()?;
252        for stmt in run_body.stmts.iter() {
253            let AstStmtKind::Assembly(asm) = &stmt.kind else { continue };
254            for ystmt in asm.block.stmts.iter() {
255                if let yul::StmtKind::Expr(e) = &ystmt.kind
256                    && let yul::ExprKind::Call(call) = &e.kind
257                    && call.name.as_str() == "return"
258                {
259                    return Some(ystmt.span);
260                }
261            }
262        }
263        None
264    }
265
266    /// If the last statement of the REPL `run()` function is an `assembly { ... }` block,
267    /// returns the span of its last non-`let` (i.e. non-VarDecl) Yul statement.
268    ///
269    /// This mirrors the legacy behavior used to pick a meaningful end-of-function PC when
270    /// the trailing statement is inline assembly.
271    fn trailing_assembly_last_stmt_span(&self) -> Option<Span> {
272        let run_body = self.repl_run_ast_body()?;
273        let AstStmtKind::Assembly(asm) = &run_body.stmts.last()?.kind else { return None };
274        asm.block
275            .stmts
276            .iter()
277            .rev()
278            .find(|s| !matches!(s.kind, yul::StmtKind::VarDecl(_, _)))
279            .map(|s| s.span)
280    }
281}
282
283/// Configuration for the [SessionSource]
284#[derive(Clone, Debug, Default, Serialize, Deserialize)]
285#[serde(bound = "")]
286pub struct SessionSourceConfig<FEN: FoundryEvmNetwork> {
287    /// Foundry configuration
288    pub foundry_config: Config,
289    /// EVM Options
290    pub evm_opts: EvmOpts,
291    /// Network family to restore when leaving fork mode.
292    #[serde(default)]
293    pub local_networks: Option<NetworkConfigs>,
294    /// Chain ID to restore when leaving fork mode.
295    #[serde(default)]
296    pub local_chain_id: Option<u64>,
297    /// Whether the saved fork network was inferred from its endpoint.
298    #[serde(default)]
299    pub fork_network_is_inferred: bool,
300    /// Whether the saved chain ID was inferred from its endpoint.
301    #[serde(default)]
302    pub fork_chain_id_is_inferred: bool,
303    /// Exact network hardfork selected for the latest execution.
304    #[serde(skip)]
305    pub resolved_hardfork: Option<FoundryHardfork>,
306    /// Source chain used for trace decoding and external identifiers.
307    #[serde(skip)]
308    pub source_chain_id: Option<u64>,
309    /// Disable the default `Vm` import.
310    pub no_vm: bool,
311    /// Cached execution backend and its fork identity.
312    #[serde(skip)]
313    pub(crate) cached_backend: Option<CachedBackend<FEN>>,
314    /// Optionally enable traces for the REPL contract execution
315    pub traces: bool,
316    /// Optionally set calldata for the REPL contract execution
317    pub calldata: Option<Vec<u8>>,
318    /// Enable viaIR with minimum optimization
319    ///
320    /// This can fix most of the "stack too deep" errors while resulting a
321    /// relatively accurate source map.
322    pub ir_minimum: bool,
323}
324
325impl<FEN: FoundryEvmNetwork> SessionSourceConfig<FEN> {
326    /// Captures the local execution context for sessions saved before it was persisted explicitly.
327    pub fn initialize_local_context(&mut self) {
328        self.evm_opts.fork_network_is_inferred = self.fork_network_is_inferred;
329        self.evm_opts.fork_chain_id_is_inferred = self.fork_chain_id_is_inferred;
330        if self.local_networks.is_none() {
331            self.local_networks = Some(self.evm_opts.networks);
332            self.local_chain_id =
333                self.evm_opts.env.chain_id.or(self.foundry_config.chain.map(|chain| chain.id()));
334        }
335    }
336
337    /// Detect the solc version to know if VM can be injected.
338    pub fn detect_solc(&mut self) -> Result<()> {
339        if self.foundry_config.solc.is_none() {
340            let version = Solc::ensure_installed(&"*".parse().unwrap())?;
341            self.foundry_config.solc = Some(SolcReq::Version(version));
342        }
343        if !self.no_vm
344            && let Some(version) = self.foundry_config.solc_version()
345            && version < MIN_VM_VERSION
346        {
347            info!(%version, minimum=%MIN_VM_VERSION, "Disabling VM injection");
348            self.no_vm = true;
349        }
350        Ok(())
351    }
352}
353
354/// REPL Session Source wrapper
355///
356/// Heavily based on soli's [`ConstructedSource`](https://github.com/jpopesculian/soli/blob/master/src/main.rs#L166)
357#[derive(Debug, Serialize, Deserialize)]
358#[serde(bound = "")]
359pub struct SessionSource<FEN: FoundryEvmNetwork> {
360    /// The file name
361    pub file_name: String,
362    /// The contract name
363    pub contract_name: String,
364
365    /// Session Source configuration
366    pub config: SessionSourceConfig<FEN>,
367
368    /// Global level Solidity code.
369    ///
370    /// Above and outside all contract declarations, in the global context.
371    pub global_code: String,
372    /// Top level Solidity code.
373    ///
374    /// Within the contract declaration, but outside of the `run()` function.
375    pub contract_code: String,
376    /// The code to be executed in the `run()` function.
377    pub run_code: String,
378
379    /// Cached VM source code.
380    #[serde(skip, default = "vm_source")]
381    vm_source: Source,
382    /// The generated output
383    #[serde(skip)]
384    output: OnceCell<GeneratedOutput>,
385}
386
387fn vm_source() -> Source {
388    Source::new(VM_SOURCE)
389}
390
391impl<FEN: FoundryEvmNetwork> Clone for SessionSource<FEN> {
392    fn clone(&self) -> Self {
393        Self {
394            file_name: self.file_name.clone(),
395            contract_name: self.contract_name.clone(),
396            global_code: self.global_code.clone(),
397            contract_code: self.contract_code.clone(),
398            run_code: self.run_code.clone(),
399            config: self.config.clone(),
400            vm_source: self.vm_source.clone(),
401            output: Default::default(),
402        }
403    }
404}
405
406impl<FEN: FoundryEvmNetwork> SessionSource<FEN> {
407    /// Creates a new source given a solidity compiler version
408    ///
409    /// # Panics
410    ///
411    /// If no Solc binary is set, cannot be found or the `--version` command fails
412    ///
413    /// ### Takes
414    ///
415    /// - An instance of [Solc]
416    /// - An instance of [SessionSourceConfig]
417    ///
418    /// ### Returns
419    ///
420    /// A new instance of [SessionSource]
421    pub fn new(mut config: SessionSourceConfig<FEN>) -> Result<Self> {
422        config.detect_solc()?;
423        Ok(Self {
424            file_name: "ReplContract.sol".to_string(),
425            contract_name: "REPL".to_string(),
426            config,
427            global_code: Default::default(),
428            contract_code: Default::default(),
429            run_code: Default::default(),
430            vm_source: vm_source(),
431            output: Default::default(),
432        })
433    }
434
435    /// Clones the [SessionSource] and appends a new line of code.
436    ///
437    /// Returns `true` if the new line was added to `run()`.
438    pub fn clone_with_new_line(&self, mut content: String) -> Result<(Self, bool)> {
439        if let Some((new_source, fragment)) = self
440            .parse_fragment(&content)
441            .or_else(|| {
442                content.push(';');
443                self.parse_fragment(&content)
444            })
445            .or_else(|| {
446                content = content.trim_end().trim_end_matches(';').to_string();
447                self.parse_fragment(&content)
448            })
449        {
450            Ok((new_source, matches!(fragment, ParseTreeFragment::Function)))
451        } else {
452            eyre::bail!("\"{}\"", content.trim());
453        }
454    }
455
456    /// Parses a fragment of Solidity code in memory and assigns it a scope within the
457    /// [`SessionSource`].
458    fn parse_fragment(&self, buffer: &str) -> Option<(Self, ParseTreeFragment)> {
459        #[track_caller]
460        fn debug_errors(errors: &EmittedDiagnostics) {
461            debug!("{errors}");
462        }
463
464        let mut this = self.clone();
465        match this.add_run_code(buffer).parse() {
466            Ok(()) => return Some((this, ParseTreeFragment::Function)),
467            Err(e) => debug_errors(&e),
468        }
469        this = self.clone();
470        match this.add_contract_code(buffer).parse() {
471            Ok(()) => return Some((this, ParseTreeFragment::Contract)),
472            Err(e) => debug_errors(&e),
473        }
474        this = self.clone();
475        match this.add_global_code(buffer).parse() {
476            Ok(()) => return Some((this, ParseTreeFragment::Source)),
477            Err(e) => debug_errors(&e),
478        }
479        None
480    }
481
482    /// Append global-level code to the source.
483    pub fn add_global_code(&mut self, content: &str) -> &mut Self {
484        self.global_code.push_str(content.trim());
485        self.global_code.push('\n');
486        self.clear_output();
487        self
488    }
489
490    /// Append contract-level code to the source.
491    pub fn add_contract_code(&mut self, content: &str) -> &mut Self {
492        self.contract_code.push_str(content.trim());
493        self.contract_code.push('\n');
494        self.clear_output();
495        self
496    }
497
498    /// Append code to the `run()` function of the REPL contract.
499    pub fn add_run_code(&mut self, content: &str) -> &mut Self {
500        self.run_code.push_str(content.trim());
501        self.run_code.push('\n');
502        self.clear_output();
503        self
504    }
505
506    /// Clears all source code.
507    pub fn clear(&mut self) {
508        String::clear(&mut self.global_code);
509        String::clear(&mut self.contract_code);
510        String::clear(&mut self.run_code);
511        self.clear_output();
512    }
513
514    /// Clear the `run()` function code.
515    pub fn clear_run(&mut self) -> &mut Self {
516        String::clear(&mut self.run_code);
517        self.clear_output();
518        self
519    }
520
521    fn clear_output(&mut self) {
522        self.output.take();
523    }
524
525    /// Compiles the source if necessary.
526    pub fn build(&self) -> Result<&GeneratedOutput> {
527        // TODO: mimics `get_or_try_init`
528        if let Some(output) = self.output.get() {
529            return Ok(output);
530        }
531        let output = self.compile()?;
532        let output = GeneratedOutput { output };
533        Ok(self.output.get_or_init(|| output))
534    }
535
536    /// Compiles the source.
537    #[cold]
538    fn compile(&self) -> Result<ProjectCompileOutput> {
539        let sources = self.get_sources();
540
541        let mut project = self.config.foundry_config.ephemeral_project()?;
542        self.config.foundry_config.disable_optimizations(&mut project, self.config.ir_minimum);
543        let mut output = ProjectCompiler::with_sources(&project, sources)?.compile()?;
544
545        if output.has_compiler_errors() {
546            eyre::bail!("{output}");
547        }
548
549        // Drive HIR lowering and analysis so that subsequent `enter` queries can use them.
550        // Chisel inspects expression values, so enable Solar's expression type table.
551        let compiler = output.parser_mut().solc_mut().compiler_mut();
552        compiler.enter_mut(|c| {
553            let _ = c.lower_asts();
554            let _ = c.analysis();
555        });
556
557        Ok(output)
558    }
559
560    fn get_sources(&self) -> Sources {
561        let mut sources = Sources::new();
562
563        let src = self.to_repl_source();
564        sources.insert(self.file_name.clone().into(), Source::new(src));
565
566        // Include Vm.sol if forge-std remapping is not available.
567        if !self.config.no_vm
568            && !self
569                .config
570                .foundry_config
571                .get_all_remappings()
572                .any(|r| r.name.starts_with("forge-std"))
573        {
574            sources.insert("forge-std/Vm.sol".into(), self.vm_source.clone());
575        }
576
577        sources
578    }
579
580    /// Construct the REPL source.
581    pub fn to_repl_source(&self) -> String {
582        let Self {
583            contract_name,
584            global_code,
585            contract_code: top_level_code,
586            run_code,
587            config,
588            ..
589        } = self;
590        let (mut vm_import, mut vm_constant) = (String::new(), String::new());
591        // Check if there's any `forge-std` remapping and determine proper path to it by
592        // searching remapping path.
593        if !config.no_vm
594            && let Some(remapping) = config
595                .foundry_config
596                .remappings
597                .iter()
598                .find(|remapping| remapping.name == "forge-std/")
599            && let Some(vm_path) = WalkDir::new(&remapping.path.path)
600                .into_iter()
601                .filter_map(|e| e.ok())
602                .find(|e| e.file_name() == "Vm.sol")
603        {
604            vm_import = format!(
605                "import {{Vm}} from \"{}\";\n",
606                vm_path.path().to_string_lossy().replace('\\', "/")
607            );
608            vm_constant = "Vm internal constant vm = Vm(address(uint160(uint256(keccak256(\"hevm cheat code\")))));\n".to_string();
609        }
610
611        format!(
612            r#"
613// SPDX-License-Identifier: UNLICENSED
614pragma solidity 0;
615
616{vm_import}
617{global_code}
618
619contract {contract_name} {{
620    {vm_constant}
621    {top_level_code}
622
623    /// @notice REPL contract entry point
624    function run() public {{
625        {run_code}
626    }}
627}}"#,
628        )
629    }
630
631    /// Parse the current source in memory using Solar.
632    pub(crate) fn parse(&self) -> Result<(), EmittedDiagnostics> {
633        let sess =
634            solar::interface::Session::builder().with_buffer_emitter(Default::default()).build();
635        let _ = sess.enter_sequential(|| -> solar::interface::Result<()> {
636            let arena = solar::ast::Arena::new();
637            let filename = self.file_name.clone().into();
638            let src = self.to_repl_source();
639            let mut parser = solar::parse::Parser::from_source_code(&sess, &arena, filename, src)?;
640            let _ast = parser.parse_file().map_err(|e| e.emit())?;
641            Ok(())
642        });
643        sess.dcx.emitted_errors().unwrap()
644    }
645}
646
647/// A Parse Tree Fragment
648///
649/// Used to determine whether an input will go to the "run()" function,
650/// the top level of the contract, or in global scope.
651#[derive(Debug)]
652enum ParseTreeFragment {
653    /// Code for the global scope
654    Source,
655    /// Code for the top level of the contract
656    Contract,
657    /// Code for the "run()" function
658    Function,
659}
660
661#[cfg(test)]
662mod tests {
663    use super::*;
664    use foundry_compilers::artifacts::remappings::{RelativeRemapping, RelativeRemappingPathBuf};
665    use foundry_evm::core::evm::EthEvmNetwork;
666    use std::fs;
667
668    #[test]
669    fn initialize_local_context_migrates_legacy_session() {
670        let mut config = SessionSourceConfig::<EthEvmNetwork>::default();
671        config.evm_opts.networks = NetworkConfigs::with_tempo();
672        config.evm_opts.env.chain_id = Some(4217);
673
674        config.initialize_local_context();
675
676        assert_eq!(config.local_networks, Some(NetworkConfigs::with_tempo()));
677        assert_eq!(config.local_chain_id, Some(4217));
678
679        config.evm_opts.networks = NetworkConfigs::default();
680        config.evm_opts.env.chain_id = Some(1);
681        config.initialize_local_context();
682
683        assert_eq!(config.local_networks, Some(NetworkConfigs::with_tempo()));
684        assert_eq!(config.local_chain_id, Some(4217));
685    }
686
687    #[test]
688    fn serialized_session_restores_fork_inference_provenance() {
689        let config = SessionSourceConfig::<EthEvmNetwork> {
690            fork_network_is_inferred: true,
691            fork_chain_id_is_inferred: true,
692            ..Default::default()
693        };
694        let encoded = serde_json::to_string(&config).unwrap();
695        let mut decoded =
696            serde_json::from_str::<SessionSourceConfig<EthEvmNetwork>>(&encoded).unwrap();
697
698        assert!(!decoded.evm_opts.fork_network_is_inferred);
699        assert!(!decoded.evm_opts.fork_chain_id_is_inferred);
700        decoded.initialize_local_context();
701        assert!(decoded.evm_opts.fork_network_is_inferred);
702        assert!(decoded.evm_opts.fork_chain_id_is_inferred);
703    }
704
705    #[test]
706    fn legacy_session_without_rpc_transport_flags_deserializes() {
707        let config = SessionSourceConfig::<EthEvmNetwork>::default();
708        let mut legacy_session = serde_json::to_value(config).unwrap();
709        let evm_opts = legacy_session["evm_opts"].as_object_mut().expect("serialized EVM options");
710        assert!(evm_opts.remove("eth_rpc_accept_invalid_certs").is_some());
711        assert!(evm_opts.remove("eth_rpc_no_proxy").is_some());
712
713        let decoded =
714            serde_json::from_value::<SessionSourceConfig<EthEvmNetwork>>(legacy_session).unwrap();
715
716        assert!(!decoded.evm_opts.rpc_accept_invalid_certs);
717        assert!(!decoded.evm_opts.rpc_no_proxy);
718    }
719
720    /// Regression test for <https://github.com/foundry-rs/foundry/issues/14711>.
721    ///
722    /// `to_repl_source()` must use forward slashes in the Vm import path regardless of OS,
723    /// because Solidity import statements require `/` as the path separator.
724    #[test]
725    fn test_vm_import_path_uses_forward_slashes() {
726        let tmp = tempfile::tempdir().unwrap();
727        let vm_sol = tmp.path().join("Vm.sol");
728        fs::write(&vm_sol, "// dummy").unwrap();
729
730        let remapping = RelativeRemapping {
731            context: None,
732            name: "forge-std/".to_string(),
733            path: RelativeRemappingPathBuf { parent: None, path: tmp.path().to_path_buf() },
734        };
735
736        let mut config: SessionSourceConfig<EthEvmNetwork> = SessionSourceConfig {
737            foundry_config: Config {
738                solc: Some(SolcReq::Version(Version::new(0, 8, 29))),
739                remappings: vec![remapping],
740                ..Default::default()
741            },
742            ..Default::default()
743        };
744        // Pre-set solc so detect_solc() skips the ensure_installed I/O.
745        config.detect_solc().unwrap();
746
747        let source = SessionSource {
748            file_name: "ReplContract.sol".to_string(),
749            contract_name: "REPL".to_string(),
750            config,
751            global_code: Default::default(),
752            contract_code: Default::default(),
753            run_code: Default::default(),
754            vm_source: vm_source(),
755            output: Default::default(),
756        };
757
758        let repl = source.to_repl_source();
759        let import_line = repl.lines().find(|l| l.contains("import {Vm}")).unwrap();
760        assert!(
761            !import_line.contains('\\'),
762            "Vm import path must not contain backslashes, got: {import_line}"
763        );
764        assert!(import_line.contains('/'), "Vm import path must use forward slashes");
765    }
766}