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 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
36pub const MIN_VM_VERSION: Version = Version::new(0, 6, 2);
38
39static VM_SOURCE: &str = include_str!("../../../testdata/utils/Vm.sol");
41
42#[derive(Clone, Debug)]
44pub(crate) struct CachedBackend<FEN: FoundryEvmNetwork> {
45 pub(crate) backend: Backend<FEN>,
46 pub(crate) resolved_fork: Option<ResolvedFork>,
47}
48
49pub 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 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
74pub 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 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 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 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 let run_body = self.run_func_body();
130
131 let last_yul_return_span: Option<Span> = self.first_yul_return_span();
139
140 let Some(last_stmt) = run_body.last() else { return Ok(None) };
143
144 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 &run_body[run_body.len().saturating_sub(2)]
160 }
161 }
162 _ => last_stmt,
163 };
164 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 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 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 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 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 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 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#[derive(Clone, Debug, Default, Serialize, Deserialize)]
285#[serde(bound = "")]
286pub struct SessionSourceConfig<FEN: FoundryEvmNetwork> {
287 pub foundry_config: Config,
289 pub evm_opts: EvmOpts,
291 #[serde(default)]
293 pub local_networks: Option<NetworkConfigs>,
294 #[serde(default)]
296 pub local_chain_id: Option<u64>,
297 #[serde(default)]
299 pub fork_network_is_inferred: bool,
300 #[serde(default)]
302 pub fork_chain_id_is_inferred: bool,
303 #[serde(skip)]
305 pub resolved_hardfork: Option<FoundryHardfork>,
306 #[serde(skip)]
308 pub source_chain_id: Option<u64>,
309 pub no_vm: bool,
311 #[serde(skip)]
313 pub(crate) cached_backend: Option<CachedBackend<FEN>>,
314 pub traces: bool,
316 pub calldata: Option<Vec<u8>>,
318 pub ir_minimum: bool,
323}
324
325impl<FEN: FoundryEvmNetwork> SessionSourceConfig<FEN> {
326 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 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#[derive(Debug, Serialize, Deserialize)]
358#[serde(bound = "")]
359pub struct SessionSource<FEN: FoundryEvmNetwork> {
360 pub file_name: String,
362 pub contract_name: String,
364
365 pub config: SessionSourceConfig<FEN>,
367
368 pub global_code: String,
372 pub contract_code: String,
376 pub run_code: String,
378
379 #[serde(skip, default = "vm_source")]
381 vm_source: Source,
382 #[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 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 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 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 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 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 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 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 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 pub fn build(&self) -> Result<&GeneratedOutput> {
527 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 #[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 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 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 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 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 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#[derive(Debug)]
652enum ParseTreeFragment {
653 Source,
655 Contract,
657 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 #[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 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}