1use 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
37pub const MIN_VM_VERSION: Version = Version::new(0, 6, 2);
39
40static VM_SOURCE: &str = include_str!("../../../testdata/utils/Vm.sol");
42
43#[derive(Clone, Debug)]
45pub(crate) struct CachedBackend<FEN: FoundryEvmNetwork> {
46 pub(crate) backend: Backend<FEN>,
47 pub(crate) resolved_fork: Option<ResolvedFork>,
48}
49
50pub 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 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
75pub 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 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 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 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 let run_body = self.run_func_body();
131
132 let last_yul_return_span: Option<Span> = self.first_yul_return_span();
140
141 let Some(last_stmt) = run_body.last() else { return Ok(None) };
144
145 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 &run_body[run_body.len().saturating_sub(2)]
161 }
162 }
163 _ => last_stmt,
164 };
165 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 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 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 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 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 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 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#[derive(Clone, Debug, Default, Serialize, Deserialize)]
286#[serde(bound = "")]
287pub struct SessionSourceConfig<FEN: FoundryEvmNetwork> {
288 pub foundry_config: Config,
290 pub evm_opts: EvmOpts,
292 #[serde(skip)]
294 pub executor_builder: ExecutorBuilder<FEN>,
295 #[serde(default)]
297 pub local_networks: Option<NetworkConfigs>,
298 #[serde(default)]
300 pub local_chain_id: Option<u64>,
301 #[serde(default)]
303 pub fork_network_is_inferred: bool,
304 #[serde(default)]
306 pub fork_chain_id_is_inferred: bool,
307 #[serde(skip)]
309 pub resolved_hardfork: Option<FoundryHardfork>,
310 #[serde(skip)]
312 pub source_chain_id: Option<u64>,
313 pub no_vm: bool,
315 #[serde(skip)]
317 pub(crate) cached_backend: Option<CachedBackend<FEN>>,
318 pub traces: bool,
320 pub calldata: Option<Vec<u8>>,
322 pub ir_minimum: bool,
327}
328
329impl<FEN: FoundryEvmNetwork> SessionSourceConfig<FEN> {
330 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 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#[derive(Debug, Serialize, Deserialize)]
362#[serde(bound = "")]
363pub struct SessionSource<FEN: FoundryEvmNetwork> {
364 pub file_name: String,
366 pub contract_name: String,
368
369 pub config: SessionSourceConfig<FEN>,
371
372 pub global_code: String,
376 pub contract_code: String,
380 pub run_code: String,
382
383 #[serde(skip, default = "vm_source")]
385 vm_source: Source,
386 #[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 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 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 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 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 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 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 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 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 pub fn build(&self) -> Result<&GeneratedOutput> {
531 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 #[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 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 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 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 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 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#[derive(Debug)]
656enum ParseTreeFragment {
657 Source,
659 Contract,
661 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 #[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 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}