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