diff --git a/Cargo.lock b/Cargo.lock index 4660ea297..9e7c4732a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -50,7 +50,7 @@ dependencies = [ "alloy-rlp", "crc", "serde", - "thiserror", + "thiserror 2.0.17", ] [[package]] @@ -76,7 +76,7 @@ dependencies = [ "borsh", "k256", "serde", - "thiserror", + "thiserror 2.0.17", ] [[package]] @@ -99,7 +99,7 @@ dependencies = [ "serde", "serde_with", "sha2", - "thiserror", + "thiserror 2.0.17", ] [[package]] @@ -657,7 +657,7 @@ dependencies = [ "rustix 0.38.44", "serde", "serde_json", - "thiserror", + "thiserror 2.0.17", "tokio", "tower-layer", "tower-service", @@ -1592,6 +1592,81 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "ethabi" +version = "18.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7413c5f74cc903ea37386a8965a936cbeb334bd270862fdece542c1b2dcbc898" +dependencies = [ + "ethereum-types", + "hex", + "once_cell", + "regex", + "serde", + "serde_json", + "sha3", + "thiserror 1.0.69", + "uint", +] + +[[package]] +name = "ethbloom" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c22d4b5885b6aa2fe5e8b9329fb8d232bf739e434e6b87347c63bdd00c120f60" +dependencies = [ + "crunchy", + "fixed-hash", + "impl-codec", + "impl-rlp", + "impl-serde", + "scale-info", + "tiny-keccak", +] + +[[package]] +name = "ethereum-types" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d215cbf040552efcbe99a38372fe80ab9d00268e20012b79fcd0f073edd8ee" +dependencies = [ + "ethbloom", + "fixed-hash", + "impl-codec", + "impl-rlp", + "impl-serde", + "primitive-types", + "scale-info", + "uint", +] + +[[package]] +name = "ethers-core" +version = "2.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d80cc6ad30b14a48ab786523af33b37f28a8623fc06afd55324816ef18fb1f" +dependencies = [ + "arrayvec", + "bytes", + "chrono", + "const-hex", + "elliptic-curve", + "ethabi", + "generic-array", + "k256", + "num_enum", + "open-fastrlp", + "rand 0.8.5", + "rlp", + "serde", + "serde_json", + "strum", + "tempfile", + "thiserror 1.0.69", + "tiny-keccak", + "unicode-xid", +] + [[package]] name = "event-listener" version = "2.5.3" @@ -1716,6 +1791,22 @@ dependencies = [ "url", ] +[[package]] +name = "fe-contract-harness" +version = "0.1.0" +dependencies = [ + "ethers-core", + "fe-codegen", + "fe-common", + "fe-driver", + "fe-solc-runner", + "hex", + "revm", + "serde_json", + "thiserror 1.0.69", + "url", +] + [[package]] name = "fe-driver" version = "0.26.0" @@ -1846,9 +1937,8 @@ dependencies = [ name = "fe-solc-runner" version = "0.1.0" dependencies = [ - "hex", + "fe-contract-harness", "indexmap 2.12.1", - "revm", "serde_json", ] @@ -2366,6 +2456,24 @@ dependencies = [ "parity-scale-codec", ] +[[package]] +name = "impl-rlp" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28220f89297a075ddc7245cd538076ee98b01f2a9c23a53a4f1105d5a322808" +dependencies = [ + "rlp", +] + +[[package]] +name = "impl-serde" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc88fc67028ae3db0c853baa36269d398d5f45b6982f95549ff5def78c935cd" +dependencies = [ + "serde", +] + [[package]] name = "impl-trait-for-tuples" version = "0.2.3" @@ -2787,6 +2895,31 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" +[[package]] +name = "open-fastrlp" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "786393f80485445794f6043fd3138854dd109cc6c4bd1a6383db304c9ce9b9ce" +dependencies = [ + "arrayvec", + "auto_impl", + "bytes", + "ethereum-types", + "open-fastrlp-derive", +] + +[[package]] +name = "open-fastrlp-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "003b2be5c6c53c1cfeb0a238b8a1c3915cd410feb684457a36c10038f764bb1c" +dependencies = [ + "bytes", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "ordermap" version = "0.5.12" @@ -3064,6 +3197,9 @@ checksum = "0b34d9fd68ae0b74a41b21c03c2f62847aa0ffea044eee893b4c140b37e244e2" dependencies = [ "fixed-hash", "impl-codec", + "impl-rlp", + "impl-serde", + "scale-info", "uint", ] @@ -3502,9 +3638,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec" dependencies = [ "bytes", + "rlp-derive", "rustc-hex", ] +[[package]] +name = "rlp-derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e33d7b2abe0c340d8797fe2907d3f20d3b5ea5908683618bfe80df7f621f672a" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "rowan" version = "0.16.1" @@ -3734,6 +3882,30 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scale-info" +version = "2.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346a3b32eba2640d17a9cb5927056b08f3de90f65b72fe09402c2ad07d684d0b" +dependencies = [ + "cfg-if", + "derive_more 1.0.0", + "parity-scale-codec", + "scale-info-derive", +] + +[[package]] +name = "scale-info-derive" +version = "2.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6630024bf739e2179b91fb424b28898baf819414262c5d376677dbff1fe7ebf" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "schemars" version = "0.9.0" @@ -4106,6 +4278,28 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.111", +] + [[package]] name = "subtle" version = "2.6.1" @@ -4185,13 +4379,33 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "144f754d318415ac792f9d69fc87abbbfc043ce2ef041c60f16ad828f638717d" +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", ] [[package]] diff --git a/crates/codegen/src/lib.rs b/crates/codegen/src/lib.rs index 2e7b4efbe..c730ce736 100644 --- a/crates/codegen/src/lib.rs +++ b/crates/codegen/src/lib.rs @@ -1,3 +1,3 @@ mod yul; -pub use yul::{YulError, emit_module_yul}; +pub use yul::{EmitModuleError, YulError, emit_module_yul}; diff --git a/crates/codegen/src/yul/doc.rs b/crates/codegen/src/yul/doc.rs index b9b3aa9a7..0e8a0d044 100644 --- a/crates/codegen/src/yul/doc.rs +++ b/crates/codegen/src/yul/doc.rs @@ -1,4 +1,5 @@ /// Simple document tree used to build readable Yul output with indentation. +#[derive(Clone)] pub(super) enum YulDoc { /// Single line of text. Line(String), @@ -36,7 +37,11 @@ pub(super) fn render_docs(nodes: &[YulDoc], indent: usize, out: &mut Vec for node in nodes { match node { YulDoc::Line(text) => { - out.push(format!("{}{}", " ".repeat(indent), text)); + if text.is_empty() { + out.push(String::new()); + } else { + out.push(format!("{}{}", " ".repeat(indent), text)); + } } YulDoc::Block { caption, body } => { let indent_str = " ".repeat(indent); diff --git a/crates/codegen/src/yul/emitter.rs b/crates/codegen/src/yul/emitter.rs deleted file mode 100644 index 58fd1bf0a..000000000 --- a/crates/codegen/src/yul/emitter.rs +++ /dev/null @@ -1,1026 +0,0 @@ -use driver::DriverDataBase; -use hir::hir_def::{ - Body, CallableDef, Expr, ExprId, Func, LitKind, Partial, Pat, PatId, PathId, Stmt, StmtId, - TopLevelMod, - expr::{ArithBinOp, BinOp, CompBinOp, LogicalBinOp, UnOp}, -}; -use mir::{ - BasicBlockId, CallOrigin, LoopInfo, MirFunction, Terminator, ValueId, ValueOrigin, - ir::{ - IntrinsicOp, IntrinsicValue, MatchArmPattern, SwitchOrigin, SwitchTarget, SwitchValue, - SyntheticValue, - }, - lower_module, -}; -use rustc_hash::FxHashMap; - -use super::{ - doc::{YulDoc, render_docs}, - errors::YulError, - state::BlockState, -}; - -/// Emits Yul for every function in the lowered MIR module. -pub fn emit_module_yul( - db: &DriverDataBase, - top_mod: TopLevelMod<'_>, -) -> Result>, mir::MirLowerError> { - let module = lower_module(db, top_mod)?; - Ok(module - .functions - .iter() - .map(|func| YulEmitter::new(db, func).and_then(|emitter| emitter.emit())) - .collect()) -} - -/// Lowers a single MIR function into the Yul document tree. -struct YulEmitter<'db> { - db: &'db DriverDataBase, - mir_func: &'db MirFunction<'db>, - body: Body<'db>, - /// Temporaries allocated for expression values that must be re-used later (e.g. struct ptrs). - expr_temps: FxHashMap, - match_values: FxHashMap, -} - -#[derive(Clone, Copy)] -/// Captures where `break`/`continue` should target when rendering loops. -struct LoopEmitCtx { - continue_target: BasicBlockId, - break_target: BasicBlockId, - implicit_continue: Option, -} - -impl<'db> YulEmitter<'db> { - fn new(db: &'db DriverDataBase, mir_func: &'db MirFunction<'db>) -> Result { - let body = mir_func - .func - .body(db) - .ok_or_else(|| YulError::MissingBody(function_name(db, mir_func.func)))?; - Ok(Self { - db, - mir_func, - body, - expr_temps: FxHashMap::default(), - match_values: FxHashMap::default(), - }) - } - - /// Produces the final Yul text for the current MIR function. - fn emit(mut self) -> Result { - let func_name = self.mir_func.symbol_name.as_str(); - let (param_names, mut state) = self.init_function_state(); - let body_docs = self.emit_block(self.mir_func.body.entry, &mut state)?; - let mut lines = Vec::new(); - render_docs(&body_docs, 4, &mut lines); - let body_text = lines.join("\n"); - Ok(format!( - "{{\n {} {{\n{body_text}\n }}\n}}", - self.format_function_signature(func_name, ¶m_names) - )) - } - - /// Initializes the `BlockState` with parameter bindings and returns their Yul names. - fn init_function_state(&self) -> (Vec, BlockState) { - let mut state = BlockState::new(); - let mut params_out = Vec::new(); - for (idx, param) in self.mir_func.func.params(self.db).enumerate() { - let original = param - .name(self.db) - .map(|id| id.data(self.db).to_string()) - .unwrap_or_else(|| format!("arg{idx}")); - let yul_name = original.clone(); - params_out.push(yul_name.clone()); - state.insert_binding(original, yul_name); - } - (params_out, state) - } - - /// Formats the Fe function name and parameters into a Yul signature. - fn format_function_signature(&self, func_name: &str, params: &[String]) -> String { - let params_str = params.join(", "); - if params.is_empty() { - format!("function {func_name}() -> ret") - } else { - format!("function {func_name}({params_str}) -> ret") - } - } - - /// Emits the Yul docs for a basic block starting without any active loop context. - fn emit_block( - &mut self, - block_id: BasicBlockId, - state: &mut BlockState, - ) -> Result, YulError> { - self.emit_block_internal(block_id, None, state, None) - } - - /// Emits a block while honoring the provided loop context (if any). - fn emit_block_with_ctx( - &mut self, - block_id: BasicBlockId, - loop_ctx: Option, - state: &mut BlockState, - ) -> Result, YulError> { - self.emit_block_internal(block_id, loop_ctx, state, None) - } - - /// Emits a block while preventing recursion into `stop_block`. - /// - /// Returns the rendered docs for `block_id`, stopping early when a branch would - /// enter `stop_block` (used for match arms that should not re-render the merge). - fn emit_block_with_stop( - &mut self, - block_id: BasicBlockId, - loop_ctx: Option, - state: &mut BlockState, - stop_block: Option, - ) -> Result, YulError> { - self.emit_block_internal(block_id, loop_ctx, state, stop_block) - } - - /// Core implementation shared by the various block emitters. - /// - /// Returns the rendered docs starting at `block_id`, honoring `loop_ctx` and - /// skipping emission entirely if `block_id == stop_block`. - fn emit_block_internal( - &mut self, - block_id: BasicBlockId, - loop_ctx: Option, - state: &mut BlockState, - stop_block: Option, - ) -> Result, YulError> { - if Some(block_id) == stop_block { - return Ok(Vec::new()); - } - let block = self - .mir_func - .body - .blocks - .get(block_id.index()) - .ok_or_else(|| YulError::Unsupported("invalid block".into()))?; - - if let Terminator::Switch { - origin: SwitchOrigin::MatchExpr(expr_id), - .. - } = &block.terminator - && !self.expr_is_unit(*expr_id) - { - self.match_values - .entry(*expr_id) - .or_insert_with(|| state.alloc_local()); - } - - let mut docs = self.render_statements(&block.insts, state)?; - match block.terminator { - Terminator::Return(Some(val)) => { - if self.emit_intrinsic_return(val, &mut docs, state)? { - return Ok(docs); - } - let value = self.mir_func.body.value(val); - if value.ty.is_tuple(self.db) && value.ty.field_count(self.db) == 0 { - docs.push(YulDoc::line("ret := 0")); - return Ok(docs); - } - let expr = match &value.origin { - ValueOrigin::Expr(expr_id) => { - self.lower_expr_with_statements(*expr_id, &mut docs, state)? - } - _ => self.lower_value(val, state)?, - }; - docs.push(YulDoc::line(format!("ret := {expr}"))); - Ok(docs) - } - Terminator::Branch { - cond, - then_bb, - else_bb, - } => { - let cond_expr = self.lower_value(cond, state)?; - let cond_temp = state.alloc_local(); - docs.push(YulDoc::line(format!("let {cond_temp} := {cond_expr}"))); - let mut then_state = state.clone(); - let mut else_state = state.clone(); - let then_docs = self.emit_block_with_ctx(then_bb, loop_ctx, &mut then_state)?; - docs.push(YulDoc::block(format!("if {cond_temp} "), then_docs)); - let else_docs = self.emit_block_with_ctx(else_bb, loop_ctx, &mut else_state)?; - docs.push(YulDoc::block(format!("if iszero({cond_temp}) "), else_docs)); - Ok(docs) - } - Terminator::Switch { - discr, - ref targets, - default, - origin, - } => match origin { - SwitchOrigin::MatchExpr(expr_id) => { - let discr_expr = self.lower_value(discr, state)?; - if self.expr_is_unit(expr_id) { - docs.push(YulDoc::line(format!("switch {discr_expr}"))); - let merge_block = self.match_merge_block(targets, default)?; - for target in targets { - let mut case_state = state.clone(); - let case_docs = self.emit_block_with_stop( - target.block, - loop_ctx, - &mut case_state, - merge_block, - )?; - let literal = switch_value_literal(&target.value); - docs.push(YulDoc::wide_block(format!(" case {literal} "), case_docs)); - } - let mut default_state = state.clone(); - let default_docs = self.emit_block_with_stop( - default, - loop_ctx, - &mut default_state, - merge_block, - )?; - docs.push(YulDoc::wide_block(" default ", default_docs)); - if let Some(merge_block) = merge_block { - let next_docs = - self.emit_block_with_ctx(merge_block, loop_ctx, state)?; - docs.extend(next_docs); - } - Ok(docs) - } else { - let temp = self - .match_values - .get(&expr_id) - .cloned() - .expect("match temp must exist"); - let match_info = - self.mir_func.body.match_info(expr_id).ok_or_else(|| { - YulError::Unsupported( - "missing match lowering info for switch".into(), - ) - })?; - - docs.push(YulDoc::line(format!("let {temp} := 0"))); - docs.push(YulDoc::line(format!("switch {discr_expr}"))); - - let mut default_body = None; - for arm in &match_info.arms { - match &arm.pattern { - MatchArmPattern::Literal(value) => { - let body_expr = self.lower_expr(arm.body, state)?; - let literal = switch_value_literal(value); - docs.push(YulDoc::wide_block( - format!(" case {literal} "), - vec![YulDoc::line(format!("{temp} := {body_expr}"))], - )); - } - MatchArmPattern::Enum { variant_index, .. } => { - let body_expr = self.lower_expr(arm.body, state)?; - let literal = - switch_value_literal(&SwitchValue::Enum(*variant_index)); - docs.push(YulDoc::wide_block( - format!(" case {literal} "), - vec![YulDoc::line(format!("{temp} := {body_expr}"))], - )); - } - MatchArmPattern::Wildcard => { - let body_expr = self.lower_expr(arm.body, state)?; - default_body = Some(body_expr); - } - } - } - - let merge_block = self.match_merge_block(targets, default)?; - if let Some(default_expr) = default_body { - docs.push(YulDoc::wide_block( - " default ", - vec![YulDoc::line(format!("{temp} := {default_expr}"))], - )); - } else { - let default_block = self - .mir_func - .body - .blocks - .get(default.index()) - .ok_or_else(|| { - YulError::Unsupported("invalid block in match lowering".into()) - })?; - if !matches!(default_block.terminator, Terminator::Unreachable) { - return Err(YulError::Unsupported( - "match lowering missing wildcard arm".into(), - )); - } - let mut default_state = state.clone(); - let default_docs = self.emit_block_with_stop( - default, - loop_ctx, - &mut default_state, - merge_block, - )?; - docs.push(YulDoc::wide_block(" default ", default_docs)); - } - if let Some(merge_block) = merge_block { - let next_docs = - self.emit_block_with_ctx(merge_block, loop_ctx, state)?; - docs.extend(next_docs); - } - Ok(docs) - } - } - SwitchOrigin::None => { - let discr_expr = self.lower_value(discr, state)?; - docs.push(YulDoc::line(format!("switch {discr_expr}"))); - for target in targets { - let mut case_state = state.clone(); - let literal = switch_value_literal(&target.value); - let case_docs = - self.emit_block_with_ctx(target.block, loop_ctx, &mut case_state)?; - docs.push(YulDoc::wide_block(format!(" case {literal} "), case_docs)); - } - let mut default_state = state.clone(); - - let default_docs = - self.emit_block_with_ctx(default, loop_ctx, &mut default_state)?; - docs.push(YulDoc::wide_block(" default ", default_docs)); - Ok(docs) - } - }, - Terminator::Unreachable => Ok(docs), - Terminator::Goto { target } => { - if let Some(ctx) = loop_ctx { - if target == ctx.continue_target { - if ctx.implicit_continue == Some(block_id) { - return Ok(docs); - } - docs.push(YulDoc::line("continue")); - return Ok(docs); - } - if target == ctx.break_target { - docs.push(YulDoc::line("break")); - return Ok(docs); - } - } - - if let Some(loop_info) = self.loop_info(target) { - let mut loop_state = state.clone(); - let (loop_doc, exit_block) = - self.emit_loop(target, loop_info, &mut loop_state)?; - docs.push(loop_doc); - let after_docs = self.emit_block_with_ctx(exit_block, loop_ctx, state)?; - docs.extend(after_docs); - return Ok(docs); - } - let next_docs = self.emit_block_with_ctx(target, loop_ctx, state)?; - docs.extend(next_docs); - Ok(docs) - } - Terminator::Return(None) => { - docs.push(YulDoc::line("ret := 0")); - Ok(docs) - } - } - } - - /// Finds the unified merge block that all literal match arms jump to, if any. - fn match_merge_block( - &self, - targets: &[SwitchTarget], - default: BasicBlockId, - ) -> Result, YulError> { - let mut merge = None; - for block_id in targets - .iter() - .map(|target| target.block) - .chain(std::iter::once(default)) - { - let block = self - .mir_func - .body - .blocks - .get(block_id.index()) - .ok_or_else(|| YulError::Unsupported("invalid block in match lowering".into()))?; - match block.terminator { - Terminator::Goto { target } => match merge { - Some(existing) if existing != target => { - return Err(YulError::Unsupported( - "match arms must converge to a single merge block".into(), - )); - } - None => merge = Some(target), - _ => {} - }, - Terminator::Unreachable => {} - _ => { - return Err(YulError::Unsupported( - "match arms must jump to a merge block".into(), - )); - } - } - } - Ok(merge) - } - - /// Looks up metadata about the loop that starts at `header`, if it exists. - fn loop_info(&self, header: BasicBlockId) -> Option { - self.mir_func.body.loop_headers.get(&header).copied() - } - - /// Emits a Yul `for` loop for the given header block and returns the exit block. - fn emit_loop( - &mut self, - header: BasicBlockId, - info: LoopInfo, - state: &mut BlockState, - ) -> Result<(YulDoc, BasicBlockId), YulError> { - let block = self - .mir_func - .body - .blocks - .get(header.index()) - .ok_or_else(|| YulError::Unsupported("invalid loop header".into()))?; - let Terminator::Branch { - cond, - then_bb, - else_bb, - } = block.terminator - else { - return Err(YulError::Unsupported( - "loop header missing branch terminator".into(), - )); - }; - if then_bb != info.body || else_bb != info.exit { - return Err(YulError::Unsupported( - "loop metadata inconsistent with terminator".into(), - )); - } - let cond_expr = self.lower_value(cond, state)?; - let loop_ctx = LoopEmitCtx { - continue_target: header, - break_target: info.exit, - implicit_continue: info.backedge, - }; - let body_docs = self.emit_block_with_ctx(info.body, Some(loop_ctx), state)?; - let loop_doc = YulDoc::block(format!("for {{ }} {cond_expr} {{ }} "), body_docs); - Ok((loop_doc, info.exit)) - } - - /// Lowers straight-line MIR instructions into Yul docs. - fn render_statements( - &mut self, - insts: &[mir::MirInst<'_>], - state: &mut BlockState, - ) -> Result, YulError> { - let mut docs = Vec::new(); - for inst in insts { - match inst { - mir::MirInst::Let { pat, value, .. } => { - let binding = self.pattern_ident(*pat)?; - let yul_name = if let Some(existing) = state.binding(&binding) { - existing - } else { - let temp = state.alloc_local(); - state.insert_binding(binding.clone(), temp.clone()) - }; - let value = match value { - Some(val) => self.lower_value(*val, state)?, - None => "0".into(), - }; - docs.push(YulDoc::line(format!("let {yul_name} := {value}"))); - } - mir::MirInst::Assign { target, value, .. } => { - let binding = self.path_from_expr(*target)?; - let yul_name = state.binding(&binding).ok_or_else(|| { - YulError::Unsupported("assignment to unknown binding".into()) - })?; - let value = self.lower_value(*value, state)?; - docs.push(YulDoc::line(format!("{yul_name} := {value}"))); - } - mir::MirInst::AugAssign { - target, value, op, .. - } => { - let binding = self.path_from_expr(*target)?; - let yul_name = state.binding(&binding).ok_or_else(|| { - YulError::Unsupported("assignment to unknown binding".into()) - })?; - let rhs = self.lower_value(*value, state)?; - let assignment = match op { - ArithBinOp::Add => format!("add({yul_name}, {rhs})"), - ArithBinOp::Sub => format!("sub({yul_name}, {rhs})"), - ArithBinOp::Mul => format!("mul({yul_name}, {rhs})"), - ArithBinOp::Div => format!("div({yul_name}, {rhs})"), - ArithBinOp::Rem => format!("mod({yul_name}, {rhs})"), - ArithBinOp::Pow => format!("exp({yul_name}, {rhs})"), - ArithBinOp::LShift => format!("shl({rhs}, {yul_name})"), - ArithBinOp::RShift => format!("shr({rhs}, {yul_name})"), - ArithBinOp::BitAnd => format!("and({yul_name}, {rhs})"), - ArithBinOp::BitOr => format!("or({yul_name}, {rhs})"), - ArithBinOp::BitXor => format!("xor({yul_name}, {rhs})"), - }; - docs.push(YulDoc::line(format!("{yul_name} := {assignment}"))); - } - mir::MirInst::Eval { value, .. } => { - if let Some(doc) = self.render_eval(*value, state)? { - docs.push(doc); - } - } - mir::MirInst::EvalExpr { - expr, - value, - bind_value, - } => { - let lowered = self.lower_value(*value, state)?; - if *bind_value { - let temp = state.alloc_local(); - self.expr_temps.insert(*expr, temp.clone()); - docs.push(YulDoc::line(format!("let {temp} := {lowered}"))); - } else { - docs.push(YulDoc::line(lowered)); - } - } - mir::MirInst::IntrinsicStmt { op, args, .. } => { - let intr = IntrinsicValue { - op: *op, - args: args.clone(), - }; - if let Some(doc) = self.lower_intrinsic_stmt(&intr, state)? { - docs.push(doc); - } - } - } - } - Ok(docs) - } - - /// Lowers a MIR `ValueId` into a Yul expression string. - fn lower_value(&self, value_id: ValueId, state: &BlockState) -> Result { - let value = self.mir_func.body.value(value_id); - match &value.origin { - ValueOrigin::Expr(expr_id) => { - if let Some(temp) = self.match_values.get(expr_id) { - Ok(temp.clone()) - } else { - self.lower_expr(*expr_id, state) - } - } - ValueOrigin::Call(call) => self.lower_call_value(call, state), - ValueOrigin::Intrinsic(intr) => self.lower_intrinsic_value(intr, state), - ValueOrigin::Synthetic(synth) => self.lower_synthetic_value(synth), - _ => Err(YulError::Unsupported( - "only expression-derived values are supported".into(), - )), - } - } - - /// Emits statements for expression statements, returning a doc when work was done. - fn render_eval( - &mut self, - value_id: ValueId, - state: &mut BlockState, - ) -> Result, YulError> { - let value = self.mir_func.body.value(value_id); - match &value.origin { - ValueOrigin::Intrinsic(intr) => self.lower_intrinsic_stmt(intr, state), - _ => Ok(None), - } - } - - /// Handles `return intrinsic::(...)` for void intrinsics by emitting the - /// side effect plus a `ret := 0`. - fn emit_intrinsic_return( - &mut self, - value_id: ValueId, - docs: &mut Vec, - state: &BlockState, - ) -> Result { - let value = self.mir_func.body.value(value_id); - if let ValueOrigin::Intrinsic(intr) = &value.origin - && !intr.op.returns_value() - { - if let Some(doc) = self.lower_intrinsic_stmt(intr, state)? { - docs.push(doc); - } - docs.push(YulDoc::line("ret := 0")); - return Ok(true); - } - Ok(false) - } - - /// Converts intrinsic value-producing operations (`mload`/`sload`) into Yul. - fn lower_intrinsic_value( - &self, - intr: &IntrinsicValue, - state: &BlockState, - ) -> Result { - if !intr.op.returns_value() { - return Err(YulError::Unsupported( - "intrinsic does not yield a value".into(), - )); - } - let args = self.lower_intrinsic_args(intr, state)?; - self.expect_intrinsic_arity(intr.op, &args, 1)?; - Ok(format!("{}({})", self.intrinsic_name(intr.op), args[0])) - } - - /// Converts intrinsic statement operations (`mstore`, …) into Yul. - fn lower_intrinsic_stmt( - &self, - intr: &IntrinsicValue, - state: &BlockState, - ) -> Result, YulError> { - if intr.op.returns_value() { - return Ok(None); - } - let args = self.lower_intrinsic_args(intr, state)?; - self.expect_intrinsic_arity(intr.op, &args, 2)?; - let line = match intr.op { - IntrinsicOp::Mstore => format!("mstore({}, {})", args[0], args[1]), - IntrinsicOp::Mstore8 => format!("mstore8({}, {})", args[0], args[1]), - IntrinsicOp::Sstore => format!("sstore({}, {})", args[0], args[1]), - _ => unreachable!(), - }; - Ok(Some(YulDoc::line(line))) - } - - /// Lowers all intrinsic arguments into Yul expressions. - fn lower_intrinsic_args( - &self, - intr: &IntrinsicValue, - state: &BlockState, - ) -> Result, YulError> { - intr.args - .iter() - .map(|arg| self.lower_value(*arg, state)) - .collect() - } - - /// Emits a user-friendly error when an intrinsic is lowered with the wrong arity. - fn expect_intrinsic_arity( - &self, - op: IntrinsicOp, - args: &[String], - expected: usize, - ) -> Result<(), YulError> { - if args.len() == expected { - Ok(()) - } else { - Err(YulError::Unsupported(format!( - "intrinsic `{}` expects {expected} arguments, got {}", - self.intrinsic_name(op), - args.len() - ))) - } - } - - /// Returns the Yul builtin name for an intrinsic opcode. - fn intrinsic_name(&self, op: IntrinsicOp) -> &'static str { - match op { - IntrinsicOp::Mload => "mload", - IntrinsicOp::Mstore => "mstore", - IntrinsicOp::Mstore8 => "mstore8", - IntrinsicOp::Sload => "sload", - IntrinsicOp::Sstore => "sstore", - } - } - - /// Lowers a HIR expression into a Yul expression string. - fn lower_expr(&self, expr_id: ExprId, state: &BlockState) -> Result { - if let Some(temp) = self.expr_temps.get(&expr_id) { - return Ok(temp.clone()); - } - if let Some(temp) = self.match_values.get(&expr_id) { - return Ok(temp.clone()); - } - if let Some(value_id) = self.mir_func.body.expr_values.get(&expr_id) - && let ValueOrigin::Call(call) = &self.mir_func.body.value(*value_id).origin - { - return self.lower_call_value(call, state); - } - - let expr = self.expect_expr(expr_id)?; - match expr { - Expr::Lit(LitKind::Int(int_id)) => Ok(int_id.data(self.db).to_string()), - Expr::Lit(LitKind::Bool(value)) => Ok(if *value { "1" } else { "0" }.into()), - Expr::Lit(LitKind::String(str_id)) => Ok(format!( - "0x{}", - hex::encode(str_id.data(self.db).as_bytes()) - )), - Expr::Un(inner, op) => { - let value = self.lower_expr(*inner, state)?; - match op { - UnOp::Minus => Ok(format!("sub(0, {value})")), - UnOp::Not => Ok(format!("iszero({value})")), - UnOp::Plus => Ok(value), - UnOp::BitNot => Ok(format!("not({value})")), - } - } - Expr::Tuple(values) => { - let parts = values - .iter() - .map(|expr| self.lower_expr(*expr, state)) - .collect::, _>>()?; - Ok(format!("tuple({})", parts.join(", "))) - } - Expr::Call(callee, call_args) => { - let callee_expr = self.lower_expr(*callee, state)?; - let mut lowered_args = Vec::with_capacity(call_args.len()); - for arg in call_args { - lowered_args.push(self.lower_expr(arg.expr, state)?); - } - if let Some(arg) = try_collapse_cast_shim(&callee_expr, &lowered_args)? { - return Ok(arg); - } - if lowered_args.is_empty() { - Ok(format!("{callee_expr}()")) - } else { - Ok(format!("{callee_expr}({})", lowered_args.join(", "))) - } - } - Expr::Bin(lhs, rhs, bin_op) => match bin_op { - BinOp::Arith(op) => { - let left = self.lower_expr(*lhs, state)?; - let right = self.lower_expr(*rhs, state)?; - match op { - ArithBinOp::Add => Ok(format!("add({left}, {right})")), - ArithBinOp::Sub => Ok(format!("sub({left}, {right})")), - ArithBinOp::Mul => Ok(format!("mul({left}, {right})")), - ArithBinOp::Div => Ok(format!("div({left}, {right})")), - ArithBinOp::Rem => Ok(format!("mod({left}, {right})")), - ArithBinOp::Pow => Ok(format!("exp({left}, {right})")), - ArithBinOp::LShift => Ok(format!("shl({right}, {left})")), - ArithBinOp::RShift => Ok(format!("shr({right}, {left})")), - ArithBinOp::BitAnd => Ok(format!("and({left}, {right})")), - ArithBinOp::BitOr => Ok(format!("or({left}, {right})")), - ArithBinOp::BitXor => Ok(format!("xor({left}, {right})")), - } - } - BinOp::Comp(op) => { - let left = self.lower_expr(*lhs, state)?; - let right = self.lower_expr(*rhs, state)?; - let expr = match op { - CompBinOp::Eq => format!("eq({left}, {right})"), - CompBinOp::NotEq => format!("iszero(eq({left}, {right}))"), - CompBinOp::Lt => format!("lt({left}, {right})"), - CompBinOp::LtEq => format!("iszero(gt({left}, {right}))"), - CompBinOp::Gt => format!("gt({left}, {right})"), - CompBinOp::GtEq => format!("iszero(lt({left}, {right}))"), - }; - Ok(expr) - } - BinOp::Logical(op) => { - let left = self.lower_expr(*lhs, state)?; - let right = self.lower_expr(*rhs, state)?; - let func = match op { - LogicalBinOp::And => "and", - LogicalBinOp::Or => "or", - }; - Ok(format!("{func}({left}, {right})")) - } - _ => Err(YulError::Unsupported( - "only arithmetic/logical binary expressions are supported right now".into(), - )), - }, - Expr::Block(stmts) => { - if let Some(expr) = self.last_expr(stmts) { - self.lower_expr(expr, state) - } else { - Ok("0".into()) - } - } - Expr::Path(path) => { - let original = self - .path_ident(*path) - .ok_or_else(|| YulError::Unsupported("unsupported path expression".into()))?; - Ok(state.resolve_name(&original)) - } - Expr::Field(..) => { - if let Some(value_id) = self.mir_func.body.expr_values.get(&expr_id) { - self.lower_value(*value_id, state) - } else { - let ty = self.mir_func.typed_body.expr_ty(self.db, expr_id); - Err(YulError::Unsupported(format!( - "field expressions should be rewritten before codegen (expr type {})", - ty.pretty_print(self.db) - ))) - } - } - Expr::RecordInit(..) => { - if let Some(temp) = self.expr_temps.get(&expr_id) { - Ok(temp.clone()) - } else { - Err(YulError::Unsupported( - "record initializers should be lowered before codegen".into(), - )) - } - } - other => Err(YulError::Unsupported(format!( - "only simple expressions are supported: {other:?}" - ))), - } - } - - /// Returns the last expression statement in a block, if any. - fn last_expr(&self, stmts: &[StmtId]) -> Option { - stmts.iter().rev().find_map(|stmt_id| { - let Ok(stmt) = self.expect_stmt(*stmt_id) else { - return None; - }; - if let Stmt::Expr(expr) = stmt { - Some(*expr) - } else { - None - } - }) - } - - /// Extracts the identifier bound by a pattern. - fn pattern_ident(&self, pat_id: PatId) -> Result { - let pat = self.expect_pat(pat_id)?; - match pat { - Pat::Path(path, _) => self - .path_ident(*path) - .ok_or_else(|| YulError::Unsupported("unsupported pattern path".into())), - _ => Err(YulError::Unsupported( - "only identifier patterns are supported".into(), - )), - } - } - - /// Resolves an expression that should represent a path (e.g. assignment target). - fn path_from_expr(&self, expr_id: ExprId) -> Result { - let expr = self.expect_expr(expr_id)?; - if let Expr::Path(path) = expr { - self.path_ident(*path) - .ok_or_else(|| YulError::Unsupported("unsupported assignment target".into())) - } else { - Err(YulError::Unsupported( - "only identifier assignments are supported".into(), - )) - } - } - - /// Returns the identifier name represented by a path, if it is a plain ident. - fn path_ident(&self, path: Partial>) -> Option { - let path = path.to_opt()?; - path.as_ident(self.db) - .map(|id| id.data(self.db).to_string()) - } - - /// Fetches the expression from HIR, converting missing data into `YulError`. - fn expect_expr(&self, expr_id: ExprId) -> Result<&Expr<'db>, YulError> { - match expr_id.data(self.db, self.body) { - Partial::Present(expr) => Ok(expr), - Partial::Absent => Err(YulError::Unsupported("expression data unavailable".into())), - } - } - - /// Fetches the pattern from HIR, converting missing data into `YulError`. - fn expect_pat(&self, pat_id: PatId) -> Result<&Pat<'db>, YulError> { - match pat_id.data(self.db, self.body) { - Partial::Present(pat) => Ok(pat), - Partial::Absent => Err(YulError::Unsupported("unsupported pattern".into())), - } - } - - /// Fetches the statement from HIR, converting missing data into `YulError`. - fn expect_stmt(&self, stmt_id: StmtId) -> Result<&Stmt<'db>, YulError> { - match stmt_id.data(self.db, self.body) { - Partial::Present(stmt) => Ok(stmt), - Partial::Absent => Err(YulError::Unsupported("statement data unavailable".into())), - } - } - - /// Lowers a MIR call into a Yul function invocation. - fn lower_call_value( - &self, - call: &CallOrigin<'_>, - state: &BlockState, - ) -> Result { - let callee = if let Some(name) = &call.resolved_name { - name.clone() - } else { - let CallableDef::Func(func) = call.callable.callable_def else { - return Err(YulError::Unsupported( - "callable without hir function definition is not supported yet".into(), - )); - }; - function_name(self.db, func) - }; - let mut lowered_args = Vec::with_capacity(call.args.len()); - for &arg in &call.args { - lowered_args.push(self.lower_value(arg, state)?); - } - if let Some(arg) = try_collapse_cast_shim(&callee, &lowered_args)? { - return Ok(arg); - } - if lowered_args.is_empty() { - Ok(format!("{callee}()")) - } else { - Ok(format!("{callee}({})", lowered_args.join(", "))) - } - } - - fn lower_synthetic_value(&self, value: &SyntheticValue) -> Result { - match value { - SyntheticValue::Int(int) => Ok(int.to_string()), - SyntheticValue::Bool(flag) => Ok(if *flag { "1" } else { "0" }.into()), - } - } - - /// Lowers expressions that may require extra statements (e.g. `if`). - fn lower_expr_with_statements( - &mut self, - expr_id: ExprId, - docs: &mut Vec, - state: &mut BlockState, - ) -> Result { - if let Some(temp) = self.expr_temps.get(&expr_id) { - return Ok(temp.clone()); - } - if let Some(temp) = self.match_values.get(&expr_id) { - return Ok(temp.clone()); - } - - let expr = self.expect_expr(expr_id)?; - if let Expr::If(cond, then_expr, else_expr) = expr { - let temp = state.alloc_local(); - docs.push(YulDoc::line(format!("let {temp} := 0"))); - let cond_expr = self.lower_expr(*cond, state)?; - let then_expr_str = self.lower_expr(*then_expr, state)?; - docs.push(YulDoc::block( - format!("if {cond_expr} "), - vec![YulDoc::line(format!("{temp} := {then_expr_str}"))], - )); - if let Some(else_expr) = else_expr { - let else_expr_str = self.lower_expr(*else_expr, state)?; - docs.push(YulDoc::block( - format!("if iszero({cond_expr}) "), - vec![YulDoc::line(format!("{temp} := {else_expr_str}"))], - )); - } - Ok(temp) - } else { - self.lower_expr(expr_id, state) - } - } - - /// Returns `true` when the given expression's type is the unit tuple. - fn expr_is_unit(&self, expr_id: ExprId) -> bool { - let ty = self.mir_func.typed_body.expr_ty(self.db, expr_id); - ty.is_tuple(self.db) && ty.field_count(self.db) == 0 - } -} - -/// Translates MIR switch literal kinds into their Yul literal strings. -fn switch_value_literal(value: &SwitchValue) -> String { - match value { - SwitchValue::Bool(true) => "1".into(), - SwitchValue::Bool(false) => "0".into(), - SwitchValue::Int(int) => int.to_string(), - SwitchValue::Enum(val) => val.to_string(), - } -} - -/// Returns the display name of a function or `` if one does not exist. -fn function_name(db: &DriverDataBase, func: Func<'_>) -> String { - func.name(db) - .to_opt() - .map(|id| id.data(db).to_string()) - .unwrap_or_else(|| "".into()) -} - -/// Returns `true` when `name` matches one of the temporary casting shims -/// (`__{src}_as_{dst}`) used while the `as` syntax is unavailable. -fn is_cast_shim(name: &str) -> bool { - cast_shim_parts(name).is_some() -} - -/// Converts usages of cast shims into their lone argument so we don't emit fake calls. -fn try_collapse_cast_shim(name: &str, args: &[String]) -> Result, YulError> { - if !is_cast_shim(name) { - return Ok(None); - } - debug_assert_eq!( - args.len(), - 1, - "cast shims are expected to take a single argument" - ); - let arg = args - .first() - .cloned() - .ok_or_else(|| YulError::Unsupported("cast shim missing argument".into()))?; - Ok(Some(arg)) -} - -/// Validates that a name follows the `__{src}_as_{dst}` convention and returns the parts. -fn cast_shim_parts(name: &str) -> Option<(&str, &str)> { - let stripped = name.strip_prefix("__")?; - let (src, dst) = stripped.split_once("_as_")?; - if src.is_empty() || dst.is_empty() { - return None; - } - if !is_cast_ident(src) || !is_cast_ident(dst) { - return None; - } - Some((src, dst)) -} - -fn is_cast_ident(part: &str) -> bool { - part.chars() - .all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit()) -} diff --git a/crates/codegen/src/yul/emitter/control_flow.rs b/crates/codegen/src/yul/emitter/control_flow.rs new file mode 100644 index 000000000..18de46515 --- /dev/null +++ b/crates/codegen/src/yul/emitter/control_flow.rs @@ -0,0 +1,532 @@ +//! Helpers for lowering MIR control-flow constructs into Yul blocks. + +use hir::hir_def::ExprId; +use mir::{ + BasicBlockId, LoopInfo, Terminator, ValueId, ValueOrigin, + ir::{MatchArmPattern, SwitchOrigin, SwitchTarget, SwitchValue}, +}; + +use crate::yul::{doc::YulDoc, errors::YulError, state::BlockState}; + +use super::function::FunctionEmitter; + +/// Captures the `break`/`continue` destinations for loop lowering. +#[derive(Clone, Copy)] +pub(super) struct LoopEmitCtx { + continue_target: BasicBlockId, + break_target: BasicBlockId, + implicit_continue: Option, +} + +/// Shared mutable context passed through control-flow helpers. +pub(super) struct BlockEmitCtx<'state, 'docs> { + pub(super) loop_ctx: Option, + pub(super) state: &'state mut BlockState, + pub(super) docs: &'docs mut Vec, +} + +impl<'state, 'docs> BlockEmitCtx<'state, 'docs> { + /// Convenience helper for cloning the block state. + fn cloned_state(&self) -> BlockState { + self.state.clone() + } +} + +impl<'db> FunctionEmitter<'db> { + /// Emits the Yul docs for a basic block starting without any active loop context. + /// + /// * `block_id` - Entry block to render. + /// * `state` - Current SSA-like binding state. + /// + /// Returns the rendered statements for the block. + pub(super) fn emit_block( + &mut self, + block_id: BasicBlockId, + state: &mut BlockState, + ) -> Result, YulError> { + self.emit_block_internal(block_id, None, state, None) + } + + /// Emits a block while honoring the provided loop context (if any). + /// + /// * `block_id` - Destination to render. + /// * `loop_ctx` - Active loop metadata or `None`. + /// * `state` - Mutable block state containing bindings. + /// + /// Returns the rendered Yul docs for the block. + pub(super) fn emit_block_with_ctx( + &mut self, + block_id: BasicBlockId, + loop_ctx: Option, + state: &mut BlockState, + ) -> Result, YulError> { + self.emit_block_internal(block_id, loop_ctx, state, None) + } + + /// Emits a block while preventing recursion into `stop_block`. + /// + /// * `block_id` - Entry block to render. + /// * `loop_ctx` - Active loop metadata or `None`. + /// * `state` - Mutable binding state. + /// * `stop_block` - Optional merge block that should not be revisited. + /// + /// Returns the rendered docs, stopping before re-entering `stop_block`. + pub(super) fn emit_block_with_stop( + &mut self, + block_id: BasicBlockId, + loop_ctx: Option, + state: &mut BlockState, + stop_block: Option, + ) -> Result, YulError> { + self.emit_block_internal(block_id, loop_ctx, state, stop_block) + } + + /// Core implementation shared by the various block emitters. + /// + /// * `block_id` - Entry block. + /// * `loop_ctx` - Optional surrounding loop context. + /// * `state` - Current binding state. + /// * `stop_block` - Merge block to skip (if any). + /// + /// Returns the rendered statements produced while traversing the block. + fn emit_block_internal( + &mut self, + block_id: BasicBlockId, + loop_ctx: Option, + state: &mut BlockState, + stop_block: Option, + ) -> Result, YulError> { + if Some(block_id) == stop_block { + return Ok(Vec::new()); + } + let block = self + .mir_func + .body + .blocks + .get(block_id.index()) + .ok_or_else(|| YulError::Unsupported("invalid block".into()))?; + + if let Terminator::Switch { + origin: SwitchOrigin::MatchExpr(expr_id), + .. + } = &block.terminator + && !self.expr_is_unit(*expr_id) + { + self.match_values + .entry(*expr_id) + .or_insert_with(|| state.alloc_local()); + } + + let mut docs = self.render_statements(&block.insts, state)?; + { + let mut ctx = BlockEmitCtx { + loop_ctx, + state, + docs: &mut docs, + }; + self.emit_block_terminator(block_id, &block.terminator, &mut ctx)?; + } + Ok(docs) + } + + /// Renders the control-flow terminator for a block after its linear statements. + /// + /// * `block_id` - Current block emitting statements. + /// * `terminator` - MIR terminator describing the outgoing control flow. + /// * `ctx` - Shared mutable context spanning the block's docs and bindings. + fn emit_block_terminator( + &mut self, + block_id: BasicBlockId, + terminator: &Terminator, + ctx: &mut BlockEmitCtx<'_, '_>, + ) -> Result<(), YulError> { + match terminator { + Terminator::Return(Some(val)) => self.emit_return_with_value(*val, ctx.docs, ctx.state), + Terminator::Return(None) => { + ctx.docs.push(YulDoc::line("ret := 0")); + Ok(()) + } + Terminator::ReturnData { offset, size } => { + let offset_expr = self.lower_value(*offset, ctx.state)?; + let size_expr = self.lower_value(*size, ctx.state)?; + ctx.docs + .push(YulDoc::line(format!("return({offset_expr}, {size_expr})"))); + Ok(()) + } + Terminator::Branch { + cond, + then_bb, + else_bb, + } => self.emit_branch_terminator(*cond, *then_bb, *else_bb, ctx), + Terminator::Switch { + discr, + targets, + default, + origin, + } => self.emit_switch_terminator(*discr, targets, *default, origin, ctx), + Terminator::Goto { target } => self.emit_goto_terminator(block_id, *target, ctx), + Terminator::Unreachable => Ok(()), + } + } + + /// Emits a `ret := ...` assignment for functions returning a concrete value. + /// + /// * `value_id` - MIR value selected by the `return` terminator. + /// * `docs` - Doc list collecting emitted statements. + /// * `state` - Binding table used when lowering the return expression. + /// + /// Returns an error if the return value could not be lowered. + fn emit_return_with_value( + &mut self, + value_id: ValueId, + docs: &mut Vec, + state: &mut BlockState, + ) -> Result<(), YulError> { + if self.emit_intrinsic_return(value_id, docs, state)? { + return Ok(()); + } + let value = self.mir_func.body.value(value_id); + if value.ty.is_tuple(self.db) && value.ty.field_count(self.db) == 0 { + docs.push(YulDoc::line("ret := 0")); + return Ok(()); + } + let expr = match &value.origin { + ValueOrigin::Expr(expr_id) => self.lower_expr_with_statements(*expr_id, docs, state)?, + _ => self.lower_value(value_id, state)?, + }; + docs.push(YulDoc::line(format!("ret := {expr}"))); + Ok(()) + } + + /// Lowers an `if cond -> then else` branch terminator into Yul conditionals. + /// + /// * `cond` - MIR value representing the branch predicate. + /// * `then_bb` / `else_bb` - Successor blocks for each branch. + /// * `ctx` - Shared block context containing loop metadata and bindings. + fn emit_branch_terminator( + &mut self, + cond: ValueId, + then_bb: BasicBlockId, + else_bb: BasicBlockId, + ctx: &mut BlockEmitCtx<'_, '_>, + ) -> Result<(), YulError> { + let cond_expr = self.lower_value(cond, ctx.state)?; + let cond_temp = ctx.state.alloc_local(); + ctx.docs + .push(YulDoc::line(format!("let {cond_temp} := {cond_expr}"))); + let loop_ctx = ctx.loop_ctx; + let mut then_state = ctx.cloned_state(); + let mut else_state = ctx.cloned_state(); + let then_docs = self.emit_block_with_ctx(then_bb, loop_ctx, &mut then_state)?; + ctx.docs + .push(YulDoc::block(format!("if {cond_temp} "), then_docs)); + let else_docs = self.emit_block_with_ctx(else_bb, loop_ctx, &mut else_state)?; + ctx.docs + .push(YulDoc::block(format!("if iszero({cond_temp}) "), else_docs)); + Ok(()) + } + + /// Emits a `switch` terminator, handling both match-driven and raw switches. + /// + /// * `discr` - MIR value containing the discriminant expression. + /// * `targets` - All concrete switch targets. + /// * `default` - Default target block. + /// * `origin` - Whether this switch originated from a match expression. + /// * `ctx` - Shared block context reused across successor emission. + fn emit_switch_terminator( + &mut self, + discr: ValueId, + targets: &[SwitchTarget], + default: BasicBlockId, + origin: &SwitchOrigin, + ctx: &mut BlockEmitCtx<'_, '_>, + ) -> Result<(), YulError> { + match origin { + SwitchOrigin::MatchExpr(expr_id) => { + self.emit_match_switch(*expr_id, discr, targets, default, ctx) + } + SwitchOrigin::None => { + let discr_expr = self.lower_value(discr, ctx.state)?; + ctx.docs.push(YulDoc::line(format!("switch {discr_expr}"))); + let loop_ctx = ctx.loop_ctx; + for target in targets { + let mut case_state = ctx.cloned_state(); + let literal = switch_value_literal(&target.value); + let case_docs = + self.emit_block_with_ctx(target.block, loop_ctx, &mut case_state)?; + ctx.docs + .push(YulDoc::wide_block(format!(" case {literal} "), case_docs)); + } + let mut default_state = ctx.cloned_state(); + let default_docs = + self.emit_block_with_ctx(default, loop_ctx, &mut default_state)?; + ctx.docs + .push(YulDoc::wide_block(" default ", default_docs)); + Ok(()) + } + } + } + + /// Emits the specialized lowering used for match expressions backed by a switch. + /// + /// * `ctx` - Shared block context containing current state/docs. + fn emit_match_switch( + &mut self, + expr_id: ExprId, + discr: ValueId, + targets: &[SwitchTarget], + default: BasicBlockId, + ctx: &mut BlockEmitCtx<'_, '_>, + ) -> Result<(), YulError> { + let discr_expr = self.lower_value(discr, ctx.state)?; + if self.expr_is_unit(expr_id) { + ctx.docs.push(YulDoc::line(format!("switch {discr_expr}"))); + let merge_block = self.match_merge_block(targets, default)?; + let loop_ctx = ctx.loop_ctx; + for target in targets { + let mut case_state = ctx.cloned_state(); + let case_docs = self.emit_block_with_stop( + target.block, + loop_ctx, + &mut case_state, + merge_block, + )?; + let literal = switch_value_literal(&target.value); + ctx.docs + .push(YulDoc::wide_block(format!(" case {literal} "), case_docs)); + } + let mut default_state = ctx.cloned_state(); + let default_docs = + self.emit_block_with_stop(default, loop_ctx, &mut default_state, merge_block)?; + ctx.docs + .push(YulDoc::wide_block(" default ", default_docs)); + if let Some(merge_block) = merge_block { + let next_docs = self.emit_block_with_ctx(merge_block, loop_ctx, ctx.state)?; + ctx.docs.extend(next_docs); + } + return Ok(()); + } + + let temp = self + .match_values + .get(&expr_id) + .cloned() + .expect("match temp must exist"); + let match_info = self.mir_func.body.match_info(expr_id).ok_or_else(|| { + YulError::Unsupported("missing match lowering info for switch".into()) + })?; + + ctx.docs.push(YulDoc::line(format!("let {temp} := 0"))); + ctx.docs.push(YulDoc::line(format!("switch {discr_expr}"))); + + let mut default_body = None; + for arm in &match_info.arms { + match &arm.pattern { + MatchArmPattern::Literal(value) => { + let body_expr = self.lower_expr(arm.body, ctx.state)?; + let literal = switch_value_literal(value); + ctx.docs.push(YulDoc::wide_block( + format!(" case {literal} "), + vec![YulDoc::line(format!("{temp} := {body_expr}"))], + )); + } + MatchArmPattern::Enum { variant_index, .. } => { + let body_expr = self.lower_expr(arm.body, ctx.state)?; + let literal = switch_value_literal(&SwitchValue::Enum(*variant_index)); + ctx.docs.push(YulDoc::wide_block( + format!(" case {literal} "), + vec![YulDoc::line(format!("{temp} := {body_expr}"))], + )); + } + MatchArmPattern::Wildcard => { + let body_expr = self.lower_expr(arm.body, ctx.state)?; + default_body = Some(body_expr); + } + } + } + + let merge_block = self.match_merge_block(targets, default)?; + let loop_ctx = ctx.loop_ctx; + if let Some(default_expr) = default_body { + ctx.docs.push(YulDoc::wide_block( + " default ", + vec![YulDoc::line(format!("{temp} := {default_expr}"))], + )); + } else { + let default_block = self + .mir_func + .body + .blocks + .get(default.index()) + .ok_or_else(|| YulError::Unsupported("invalid block in match lowering".into()))?; + if !matches!(default_block.terminator, Terminator::Unreachable) { + return Err(YulError::Unsupported( + "match lowering missing wildcard arm".into(), + )); + } + let mut default_state = ctx.cloned_state(); + let default_docs = + self.emit_block_with_stop(default, loop_ctx, &mut default_state, merge_block)?; + ctx.docs + .push(YulDoc::wide_block(" default ", default_docs)); + } + if let Some(merge_block) = merge_block { + let next_docs = self.emit_block_with_ctx(merge_block, loop_ctx, ctx.state)?; + ctx.docs.extend(next_docs); + } + Ok(()) + } + + /// Handles `goto` terminators, translating loop jumps into `break`/`continue` + /// and recursively emitting successor blocks otherwise. + /// + /// * `block_id` - Current block index (used for implicit continues). + /// * `target` - Destination block selected by the `goto`. + /// * `ctx` - Shared context holding the current bindings and docs. + fn emit_goto_terminator( + &mut self, + block_id: BasicBlockId, + target: BasicBlockId, + ctx: &mut BlockEmitCtx<'_, '_>, + ) -> Result<(), YulError> { + if let Some(loop_ctx) = ctx.loop_ctx { + if target == loop_ctx.continue_target { + if loop_ctx.implicit_continue == Some(block_id) { + return Ok(()); + } + ctx.docs.push(YulDoc::line("continue")); + return Ok(()); + } + if target == loop_ctx.break_target { + ctx.docs.push(YulDoc::line("break")); + return Ok(()); + } + } + + if let Some(loop_info) = self.loop_info(target) { + let mut loop_state = ctx.cloned_state(); + let (loop_doc, exit_block) = self.emit_loop(target, loop_info, &mut loop_state)?; + ctx.docs.push(loop_doc); + let after_docs = self.emit_block_with_ctx(exit_block, ctx.loop_ctx, ctx.state)?; + ctx.docs.extend(after_docs); + return Ok(()); + } + let next_docs = self.emit_block_with_ctx(target, ctx.loop_ctx, ctx.state)?; + ctx.docs.extend(next_docs); + Ok(()) + } + + /// Finds the unified merge block that all literal match arms jump to, if any. + /// Determines if the match lowering introduced a merge block and returns it. + /// + /// * `targets` - All non-default switch destinations. + /// * `default` - Default block ID written by MIR. + /// + /// Returns the merge block when both branches converge, otherwise `None`. + fn match_merge_block( + &self, + targets: &[SwitchTarget], + default: BasicBlockId, + ) -> Result, YulError> { + let mut merge = None; + for block_id in targets + .iter() + .map(|target| target.block) + .chain(std::iter::once(default)) + { + let block = self + .mir_func + .body + .blocks + .get(block_id.index()) + .ok_or_else(|| YulError::Unsupported("invalid block in match lowering".into()))?; + match block.terminator { + Terminator::Goto { target } => match merge { + Some(existing) if existing != target => { + return Err(YulError::Unsupported( + "match arms must converge to a single merge block".into(), + )); + } + None => merge = Some(target), + _ => {} + }, + Terminator::Unreachable => {} + _ => { + return Err(YulError::Unsupported( + "match arms must jump to a merge block".into(), + )); + } + } + } + Ok(merge) + } + + /// Looks up metadata about the loop that starts at `header`, if it exists. + /// Fetches MIR loop metadata for the requested header block. + /// + /// * `header` - Loop header to query. + /// + /// Returns the associated [`LoopInfo`] when the MIR builder recorded one. + fn loop_info(&self, header: BasicBlockId) -> Option { + self.mir_func.body.loop_headers.get(&header).copied() + } + + /// Emits a Yul `for` loop for the given header block and returns the exit block. + /// + /// * `header` - Loop header block chosen by MIR. + /// * `info` - Loop metadata describing body/backedge/exit blocks. + /// * `state` - Mutable binding state used while rendering body and exit. + /// + /// Returns the loop doc plus the block ID that execution continues at after the loop exits. + fn emit_loop( + &mut self, + header: BasicBlockId, + info: LoopInfo, + state: &mut BlockState, + ) -> Result<(YulDoc, BasicBlockId), YulError> { + let block = self + .mir_func + .body + .blocks + .get(header.index()) + .ok_or_else(|| YulError::Unsupported("invalid loop header".into()))?; + let Terminator::Branch { + cond, + then_bb, + else_bb, + } = block.terminator + else { + return Err(YulError::Unsupported( + "loop header missing branch terminator".into(), + )); + }; + if then_bb != info.body || else_bb != info.exit { + return Err(YulError::Unsupported( + "loop metadata inconsistent with terminator".into(), + )); + } + let cond_expr = self.lower_value(cond, state)?; + let loop_ctx = LoopEmitCtx { + continue_target: header, + break_target: info.exit, + implicit_continue: info.backedge, + }; + let body_docs = self.emit_block_with_ctx(info.body, Some(loop_ctx), state)?; + let loop_doc = YulDoc::block(format!("for {{ }} {cond_expr} {{ }} "), body_docs); + Ok((loop_doc, info.exit)) + } +} + +/// Translates MIR switch literal kinds into their Yul literal strings. +/// +/// * `value` - Switch value representation. +/// +/// Returns the string literal used inside the `switch`. +fn switch_value_literal(value: &SwitchValue) -> String { + match value { + SwitchValue::Bool(true) => "1".into(), + SwitchValue::Bool(false) => "0".into(), + SwitchValue::Int(int) => int.to_string(), + SwitchValue::Enum(val) => val.to_string(), + } +} diff --git a/crates/codegen/src/yul/emitter/expr.rs b/crates/codegen/src/yul/emitter/expr.rs new file mode 100644 index 000000000..0ffc8f35e --- /dev/null +++ b/crates/codegen/src/yul/emitter/expr.rs @@ -0,0 +1,316 @@ +//! Expression and value lowering helpers shared across the Yul emitter. + +use hir::hir_def::{ + CallableDef, Expr, ExprId, LitKind, Stmt, StmtId, + expr::{ArithBinOp, BinOp, CompBinOp, LogicalBinOp, UnOp}, +}; +use mir::{CallOrigin, ValueId, ValueOrigin, ir::SyntheticValue}; + +use crate::yul::{doc::YulDoc, state::BlockState}; + +use super::{ + YulError, + function::FunctionEmitter, + util::{function_name, try_collapse_cast_shim}, +}; + +impl<'db> FunctionEmitter<'db> { + /// Lowers a MIR `ValueId` into a Yul expression string. + /// + /// * `value_id` - Identifier selecting the MIR value. + /// * `state` - Current bindings for previously-evaluated expressions. + /// + /// Returns the Yul expression referencing the value or an error if unsupported. + pub(super) fn lower_value( + &self, + value_id: ValueId, + state: &BlockState, + ) -> Result { + let value = self.mir_func.body.value(value_id); + match &value.origin { + ValueOrigin::Expr(expr_id) => { + if let Some(temp) = self.match_values.get(expr_id) { + Ok(temp.clone()) + } else { + self.lower_expr(*expr_id, state) + } + } + ValueOrigin::Call(call) => self.lower_call_value(call, state), + ValueOrigin::Intrinsic(intr) => self.lower_intrinsic_value(intr, state), + ValueOrigin::Synthetic(synth) => self.lower_synthetic_value(synth), + _ => Err(YulError::Unsupported( + "only expression-derived values are supported".into(), + )), + } + } + + /// Lowers a HIR expression into a Yul expression string. + /// + /// * `expr_id` - Expression to render. + /// * `state` - Binding state used for nested expressions. + /// + /// Returns the fully-lowered Yul expression. + pub(super) fn lower_expr( + &self, + expr_id: ExprId, + state: &BlockState, + ) -> Result { + if let Some(temp) = self.expr_temps.get(&expr_id) { + return Ok(temp.clone()); + } + if let Some(temp) = self.match_values.get(&expr_id) { + return Ok(temp.clone()); + } + if let Some(value_id) = self.mir_func.body.expr_values.get(&expr_id) { + let value = self.mir_func.body.value(*value_id); + match &value.origin { + ValueOrigin::Call(call) => return self.lower_call_value(call, state), + ValueOrigin::Synthetic(synth) => { + return self.lower_synthetic_value(synth); + } + _ => {} + } + } + + let expr = self.expect_expr(expr_id)?; + match expr { + Expr::Lit(LitKind::Int(int_id)) => Ok(int_id.data(self.db).to_string()), + Expr::Lit(LitKind::Bool(value)) => Ok(if *value { "1" } else { "0" }.into()), + Expr::Lit(LitKind::String(str_id)) => Ok(format!( + "0x{}", + hex::encode(str_id.data(self.db).as_bytes()) + )), + Expr::Un(inner, op) => { + let value = self.lower_expr(*inner, state)?; + match op { + UnOp::Minus => Ok(format!("sub(0, {value})")), + UnOp::Not => Ok(format!("iszero({value})")), + UnOp::Plus => Ok(value), + UnOp::BitNot => Ok(format!("not({value})")), + } + } + Expr::Tuple(values) => { + let parts = values + .iter() + .map(|expr| self.lower_expr(*expr, state)) + .collect::, _>>()?; + Ok(format!("tuple({})", parts.join(", "))) + } + Expr::Call(callee, call_args) => { + let callee_expr = self.lower_expr(*callee, state)?; + let mut lowered_args = Vec::with_capacity(call_args.len()); + for arg in call_args { + lowered_args.push(self.lower_expr(arg.expr, state)?); + } + if let Some(arg) = try_collapse_cast_shim(&callee_expr, &lowered_args)? { + return Ok(arg); + } + if lowered_args.is_empty() { + Ok(format!("{callee_expr}()")) + } else { + Ok(format!("{callee_expr}({})", lowered_args.join(", "))) + } + } + Expr::Bin(lhs, rhs, bin_op) => match bin_op { + BinOp::Arith(op) => { + let left = self.lower_expr(*lhs, state)?; + let right = self.lower_expr(*rhs, state)?; + match op { + ArithBinOp::Add => Ok(format!("add({left}, {right})")), + ArithBinOp::Sub => Ok(format!("sub({left}, {right})")), + ArithBinOp::Mul => Ok(format!("mul({left}, {right})")), + ArithBinOp::Div => Ok(format!("div({left}, {right})")), + ArithBinOp::Rem => Ok(format!("mod({left}, {right})")), + ArithBinOp::Pow => Ok(format!("exp({left}, {right})")), + ArithBinOp::LShift => Ok(format!("shl({right}, {left})")), + ArithBinOp::RShift => Ok(format!("shr({right}, {left})")), + ArithBinOp::BitAnd => Ok(format!("and({left}, {right})")), + ArithBinOp::BitOr => Ok(format!("or({left}, {right})")), + ArithBinOp::BitXor => Ok(format!("xor({left}, {right})")), + } + } + BinOp::Comp(op) => { + let left = self.lower_expr(*lhs, state)?; + let right = self.lower_expr(*rhs, state)?; + let expr = match op { + CompBinOp::Eq => format!("eq({left}, {right})"), + CompBinOp::NotEq => format!("iszero(eq({left}, {right}))"), + CompBinOp::Lt => format!("lt({left}, {right})"), + CompBinOp::LtEq => format!("iszero(gt({left}, {right}))"), + CompBinOp::Gt => format!("gt({left}, {right})"), + CompBinOp::GtEq => format!("iszero(lt({left}, {right}))"), + }; + Ok(expr) + } + BinOp::Logical(op) => { + let left = self.lower_expr(*lhs, state)?; + let right = self.lower_expr(*rhs, state)?; + let func = match op { + LogicalBinOp::And => "and", + LogicalBinOp::Or => "or", + }; + Ok(format!("{func}({left}, {right})")) + } + _ => Err(YulError::Unsupported( + "only arithmetic/logical binary expressions are supported right now".into(), + )), + }, + Expr::Block(stmts) => { + if let Some(expr) = self.last_expr(stmts) { + self.lower_expr(expr, state) + } else { + Ok("0".into()) + } + } + Expr::Path(path) => { + let original = self + .path_ident(*path) + .ok_or_else(|| YulError::Unsupported("unsupported path expression".into()))?; + Ok(state.resolve_name(&original)) + } + Expr::Field(..) => { + if let Some(value_id) = self.mir_func.body.expr_values.get(&expr_id) { + self.lower_value(*value_id, state) + } else { + let ty = self.mir_func.typed_body.expr_ty(self.db, expr_id); + Err(YulError::Unsupported(format!( + "field expressions should be rewritten before codegen (expr type {})", + ty.pretty_print(self.db) + ))) + } + } + Expr::RecordInit(..) => { + if let Some(temp) = self.expr_temps.get(&expr_id) { + Ok(temp.clone()) + } else { + Err(YulError::Unsupported( + "record initializers should be lowered before codegen".into(), + )) + } + } + other => Err(YulError::Unsupported(format!( + "only simple expressions are supported: {other:?}" + ))), + } + } + + /// Returns the last expression statement in a block, if any. + /// + /// * `stmts` - Slice of statement IDs to inspect. + /// + /// Returns the expression ID for the trailing expression statement when present. + fn last_expr(&self, stmts: &[StmtId]) -> Option { + stmts.iter().rev().find_map(|stmt_id| { + let Ok(stmt) = self.expect_stmt(*stmt_id) else { + return None; + }; + if let Stmt::Expr(expr) = stmt { + Some(*expr) + } else { + None + } + }) + } + + /// Lowers a MIR call into a Yul function invocation. + /// + /// * `call` - Call origin describing the callee and arguments. + /// * `state` - Binding state used to lower argument expressions. + /// + /// Returns the Yul invocation string for the call. + pub(super) fn lower_call_value( + &self, + call: &CallOrigin<'_>, + state: &BlockState, + ) -> Result { + let callee = if let Some(name) = &call.resolved_name { + name.clone() + } else { + match call.callable.callable_def { + CallableDef::Func(func) => function_name(self.db, func), + CallableDef::VariantCtor(_) => { + return Err(YulError::Unsupported( + "callable without hir function definition is not supported yet".into(), + )); + } + } + }; + let mut lowered_args = Vec::with_capacity(call.args.len()); + for &arg in &call.args { + lowered_args.push(self.lower_value(arg, state)?); + } + if let Some(arg) = try_collapse_cast_shim(&callee, &lowered_args)? { + return Ok(arg); + } + if lowered_args.is_empty() { + Ok(format!("{callee}()")) + } else { + Ok(format!("{callee}({})", lowered_args.join(", "))) + } + } + + /// Lowers special MIR synthetic values such as constants into Yul expressions. + /// + /// * `value` - Synthetic value emitted during MIR construction. + /// + /// Returns the literal Yul expression for the synthetic value. + fn lower_synthetic_value(&self, value: &SyntheticValue) -> Result { + match value { + SyntheticValue::Int(int) => Ok(int.to_string()), + SyntheticValue::Bool(flag) => Ok(if *flag { "1" } else { "0" }.into()), + } + } + + /// Lowers expressions that may require extra statements (e.g. `if`). + /// + /// * `expr_id` - Expression to lower. + /// * `docs` - Doc list to append emitted statements into. + /// * `state` - Binding state for allocating temporaries. + /// + /// Returns either the inline expression or the name of a temporary containing the result. + pub(super) fn lower_expr_with_statements( + &mut self, + expr_id: ExprId, + docs: &mut Vec, + state: &mut BlockState, + ) -> Result { + if let Some(temp) = self.expr_temps.get(&expr_id) { + return Ok(temp.clone()); + } + if let Some(temp) = self.match_values.get(&expr_id) { + return Ok(temp.clone()); + } + + let expr = self.expect_expr(expr_id)?; + if let Expr::If(cond, then_expr, else_expr) = expr { + let temp = state.alloc_local(); + docs.push(YulDoc::line(format!("let {temp} := 0"))); + let cond_expr = self.lower_expr(*cond, state)?; + let then_expr_str = self.lower_expr(*then_expr, state)?; + docs.push(YulDoc::block( + format!("if {cond_expr} "), + vec![YulDoc::line(format!("{temp} := {then_expr_str}"))], + )); + if let Some(else_expr) = else_expr { + let else_expr_str = self.lower_expr(*else_expr, state)?; + docs.push(YulDoc::block( + format!("if iszero({cond_expr}) "), + vec![YulDoc::line(format!("{temp} := {else_expr_str}"))], + )); + } + Ok(temp) + } else { + self.lower_expr(expr_id, state) + } + } + + /// Returns `true` when the given expression's type is the unit tuple. + /// + /// * `expr_id` - Expression identifier whose type should be tested. + /// + /// Returns `true` if the expression's type is the unit tuple. + pub(super) fn expr_is_unit(&self, expr_id: ExprId) -> bool { + let ty = self.mir_func.typed_body.expr_ty(self.db, expr_id); + ty.is_tuple(self.db) && ty.field_count(self.db) == 0 + } +} diff --git a/crates/codegen/src/yul/emitter/function.rs b/crates/codegen/src/yul/emitter/function.rs new file mode 100644 index 000000000..71e882861 --- /dev/null +++ b/crates/codegen/src/yul/emitter/function.rs @@ -0,0 +1,191 @@ +use driver::DriverDataBase; +use hir::hir_def::{Body, Expr, ExprId, Partial, Pat, PatId, PathId, Stmt, StmtId}; +use mir::MirFunction; +use rustc_hash::FxHashMap; + +use crate::yul::{doc::YulDoc, errors::YulError, state::BlockState}; + +use super::util::function_name; + +/// Emits Yul for a single MIR function. +pub(super) struct FunctionEmitter<'db> { + pub(super) db: &'db DriverDataBase, + pub(super) mir_func: &'db MirFunction<'db>, + body: Body<'db>, + /// Temporaries allocated for expression values that must be re-used later (e.g. struct ptrs). + pub(super) expr_temps: FxHashMap, + pub(super) match_values: FxHashMap, + /// Number of MIR references per value so we can avoid evaluating them twice. + pub(super) value_use_counts: Vec, +} + +impl<'db> FunctionEmitter<'db> { + /// Constructs a new emitter for the given MIR function. + /// + /// * `db` - Driver database providing access to bodies and type info. + /// * `mir_func` - MIR function to lower into Yul. + /// + /// Returns the initialized emitter or [`YulError::MissingBody`] if the + /// function lacks a body. + pub(super) fn new( + db: &'db DriverDataBase, + mir_func: &'db MirFunction<'db>, + ) -> Result { + let body = mir_func + .func + .body(db) + .ok_or_else(|| YulError::MissingBody(function_name(db, mir_func.func)))?; + let value_use_counts = Self::collect_value_use_counts(&mir_func.body); + Ok(Self { + db, + mir_func, + body, + expr_temps: FxHashMap::default(), + match_values: FxHashMap::default(), + value_use_counts, + }) + } + + /// Counts how many MIR instructions/terminators use each `ValueId`. + fn collect_value_use_counts(body: &mir::MirBody<'db>) -> Vec { + let mut counts = vec![0; body.values.len()]; + for block in &body.blocks { + for inst in &block.insts { + match inst { + mir::MirInst::Let { value, .. } => { + if let Some(value) = value { + counts[value.index()] += 1; + } + } + mir::MirInst::Assign { value, .. } + | mir::MirInst::AugAssign { value, .. } + | mir::MirInst::Eval { value, .. } + | mir::MirInst::EvalExpr { value, .. } => { + counts[value.index()] += 1; + } + mir::MirInst::IntrinsicStmt { args, .. } => { + for arg in args { + counts[arg.index()] += 1; + } + } + } + } + match &block.terminator { + mir::Terminator::Return(Some(value)) => counts[value.index()] += 1, + mir::Terminator::ReturnData { offset, size } => { + counts[offset.index()] += 1; + counts[size.index()] += 1; + } + mir::Terminator::Branch { cond, .. } => counts[cond.index()] += 1, + mir::Terminator::Switch { discr, .. } => counts[discr.index()] += 1, + mir::Terminator::Return(None) + | mir::Terminator::Goto { .. } + | mir::Terminator::Unreachable => {} + } + } + counts + } + + /// Produces the final Yul docs for the current MIR function. + /// + /// Returns the document tree containing a single Yul `function` block or a + /// [`YulError`] when lowering fails. + pub(super) fn emit_doc(mut self) -> Result, YulError> { + let func_name = self.mir_func.symbol_name.as_str(); + let (param_names, mut state) = self.init_function_state(); + let body_docs = self.emit_block(self.mir_func.body.entry, &mut state)?; + let function_doc = YulDoc::block( + format!( + "{} ", + self.format_function_signature(func_name, ¶m_names) + ), + body_docs, + ); + Ok(vec![function_doc]) + } + + /// Initializes the `BlockState` with parameter bindings and returns their Yul names. + /// + /// Returns a tuple containing the ordered argument names and the populated block state. + pub(super) fn init_function_state(&self) -> (Vec, BlockState) { + let mut state = BlockState::new(); + let mut params_out = Vec::new(); + for (idx, param) in self.mir_func.func.params(self.db).enumerate() { + let original = param + .name(self.db) + .map(|id| id.data(self.db).to_string()) + .unwrap_or_else(|| format!("arg{idx}")); + let yul_name = original.clone(); + params_out.push(yul_name.clone()); + state.insert_binding(original, yul_name); + } + (params_out, state) + } + + /// Formats the Fe function name and parameters into a Yul signature. + fn format_function_signature(&self, func_name: &str, params: &[String]) -> String { + let params_str = params.join(", "); + if params.is_empty() { + format!("function {func_name}() -> ret") + } else { + format!("function {func_name}({params_str}) -> ret") + } + } + + /// Extracts the identifier bound by a pattern. + pub(super) fn pattern_ident(&self, pat_id: PatId) -> Result { + let pat = self.expect_pat(pat_id)?; + match pat { + Pat::Path(path, _) => self + .path_ident(*path) + .ok_or_else(|| YulError::Unsupported("unsupported pattern path".into())), + _ => Err(YulError::Unsupported( + "only identifier patterns are supported".into(), + )), + } + } + + /// Resolves an expression that should represent a path (e.g. assignment target). + pub(super) fn path_from_expr(&self, expr_id: ExprId) -> Result { + let expr = self.expect_expr(expr_id)?; + if let Expr::Path(path) = expr { + self.path_ident(*path) + .ok_or_else(|| YulError::Unsupported("unsupported assignment target".into())) + } else { + Err(YulError::Unsupported( + "only identifier assignments are supported".into(), + )) + } + } + + /// Returns the identifier name represented by a path, if it is a plain ident. + pub(super) fn path_ident(&self, path: Partial>) -> Option { + let path = path.to_opt()?; + path.as_ident(self.db) + .map(|id| id.data(self.db).to_string()) + } + + /// Fetches the expression from HIR, converting missing data into `YulError`. + pub(super) fn expect_expr(&self, expr_id: ExprId) -> Result<&Expr<'db>, YulError> { + match expr_id.data(self.db, self.body) { + Partial::Present(expr) => Ok(expr), + Partial::Absent => Err(YulError::Unsupported("expression data unavailable".into())), + } + } + + /// Fetches the pattern from HIR, converting missing data into `YulError`. + pub(super) fn expect_pat(&self, pat_id: PatId) -> Result<&Pat<'db>, YulError> { + match pat_id.data(self.db, self.body) { + Partial::Present(pat) => Ok(pat), + Partial::Absent => Err(YulError::Unsupported("unsupported pattern".into())), + } + } + + /// Fetches the statement from HIR, converting missing data into `YulError`. + pub(super) fn expect_stmt(&self, stmt_id: StmtId) -> Result<&Stmt<'db>, YulError> { + match stmt_id.data(self.db, self.body) { + Partial::Present(stmt) => Ok(stmt), + Partial::Absent => Err(YulError::Unsupported("statement data unavailable".into())), + } + } +} diff --git a/crates/codegen/src/yul/emitter/mod.rs b/crates/codegen/src/yul/emitter/mod.rs new file mode 100644 index 000000000..a81524bb9 --- /dev/null +++ b/crates/codegen/src/yul/emitter/mod.rs @@ -0,0 +1,29 @@ +use std::fmt; + +use crate::yul::errors::YulError; + +pub use module::emit_module_yul; + +mod control_flow; +mod expr; +mod function; +mod module; +mod statements; +mod util; + +#[derive(Debug)] +pub enum EmitModuleError { + MirLower(mir::MirLowerError), + Yul(YulError), +} + +impl fmt::Display for EmitModuleError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + EmitModuleError::MirLower(err) => write!(f, "{err}"), + EmitModuleError::Yul(err) => write!(f, "{err}"), + } + } +} + +impl std::error::Error for EmitModuleError {} diff --git a/crates/codegen/src/yul/emitter/module.rs b/crates/codegen/src/yul/emitter/module.rs new file mode 100644 index 000000000..02081378a --- /dev/null +++ b/crates/codegen/src/yul/emitter/module.rs @@ -0,0 +1,103 @@ +//! Module-level Yul emission helpers (functions + runtime wrappers). + +use driver::DriverDataBase; +use hir::hir_def::TopLevelMod; +use mir::lower_module; + +use crate::yul::doc::{YulDoc, render_docs}; + +use super::{EmitModuleError, function::FunctionEmitter}; + +/// Emits Yul for every function in the lowered MIR module. +/// +/// * `db` - Driver database used to query compiler facts. +/// * `top_mod` - Root module to lower. +/// +/// Returns a single Yul string containing all lowered functions followed by any +/// auto-generated contract runtimes, or [`EmitModuleError`] if MIR lowering or +/// Yul emission fails. +pub fn emit_module_yul( + db: &DriverDataBase, + top_mod: TopLevelMod<'_>, +) -> Result { + let module = lower_module(db, top_mod).map_err(EmitModuleError::MirLower)?; + + // Emit Yul docs for each function + let mut function_docs: Vec> = Vec::with_capacity(module.functions.len()); + for func in &module.functions { + let emitter = FunctionEmitter::new(db, func).map_err(EmitModuleError::Yul)?; + let docs = emitter.emit_doc().map_err(EmitModuleError::Yul)?; + function_docs.push(docs); + } + + let mut docs = Vec::new(); + + if module.contracts.is_empty() { + // No contracts: emit all functions as top-level + for func_docs in function_docs { + docs.extend(func_docs); + } + } else { + // Emit each contract with its reachable functions + for contract in &module.contracts { + let mut contract_fn_docs = Vec::new(); + for &idx in &contract.function_indices { + contract_fn_docs.extend(function_docs[idx].clone()); + } + docs.extend(render_contract_runtime_docs( + &contract.name, + &contract_fn_docs, + )); + } + } + + let mut lines = Vec::new(); + render_docs(&docs, 0, &mut lines); + Ok(join_lines(lines)) +} + +/// Creates a Yul doc tree describing the dispatcher-based runtime wrapper for `name`. +/// +/// * `name` - Contract identifier used for the Yul `object`. +/// * `functions` - Yul docs for all functions reachable from this contract's dispatcher. +/// +/// Returns the doc list containing constructor and runtime sections. +fn render_contract_runtime_docs(name: &str, functions: &[YulDoc]) -> Vec { + let mut runtime_body = Vec::new(); + if !functions.is_empty() { + runtime_body.extend(functions.iter().cloned()); + runtime_body.push(YulDoc::line(String::new())); + } + // TODO: This is just temporary until we have a real dispatcher implementation. + runtime_body.push(YulDoc::line("pop(dispatch())")); + runtime_body.push(YulDoc::line("stop()")); + vec![YulDoc::block( + format!("object \"{name}\" "), + vec![ + YulDoc::block( + "code ", + vec![ + YulDoc::line("datacopy(0, dataoffset(\"runtime\"), datasize(\"runtime\"))"), + YulDoc::line("return(0, datasize(\"runtime\"))"), + ], + ), + YulDoc::line(String::new()), + YulDoc::block( + "object \"runtime\" ", + vec![YulDoc::block("code ", runtime_body)], + ), + ], + )] +} + +/// Joins rendered lines while trimming trailing whitespace-only entries. +/// +/// * `lines` - Vector of rendered Yul lines. +/// +/// Returns the normalized Yul output string. +fn join_lines(mut lines: Vec) -> String { + while lines.last().is_some_and(|line| line.is_empty()) { + lines.pop(); + } + lines.join("\n") +} diff --git a/crates/codegen/src/yul/emitter/statements.rs b/crates/codegen/src/yul/emitter/statements.rs new file mode 100644 index 000000000..664bab0f0 --- /dev/null +++ b/crates/codegen/src/yul/emitter/statements.rs @@ -0,0 +1,399 @@ +//! Helpers for lowering linear MIR statements into Yul docs. +//! +//! The functions defined in this module operate within `FunctionEmitter` and walk +//! straight-line MIR instructions (non-terminators) to produce Yul statements. + +use hir::hir_def::{ExprId, PatId, expr::ArithBinOp}; +use mir::ir::{IntrinsicOp, IntrinsicValue}; +use mir::{self, ValueId, ValueOrigin}; + +use crate::yul::{doc::YulDoc, state::BlockState}; + +use super::{YulError, function::FunctionEmitter}; + +impl<'db> FunctionEmitter<'db> { + /// Lowers a linear sequence of MIR instructions into Yul docs. + /// + /// * `insts` - MIR instructions belonging to the current block. + /// * `state` - Mutable binding table shared across the block. + /// + /// Returns all emitted Yul statements prior to the block terminator. + pub(super) fn render_statements( + &mut self, + insts: &[mir::MirInst<'_>], + state: &mut BlockState, + ) -> Result, YulError> { + let mut docs = Vec::new(); + for inst in insts { + self.emit_inst(&mut docs, inst, state)?; + } + Ok(docs) + } + + /// Dispatches an individual MIR instruction to the appropriate lowering helper. + /// + /// * `docs` - Accumulator that stores every emitted Yul statement. + /// * `inst` - Instruction being lowered. + /// * `state` - Mutable per-block binding state shared across helpers. + /// + /// Returns `Ok(())` once the instruction has been lowered. + fn emit_inst( + &mut self, + docs: &mut Vec, + inst: &mir::MirInst<'_>, + state: &mut BlockState, + ) -> Result<(), YulError> { + match inst { + mir::MirInst::Let { pat, value, .. } => { + self.emit_let_inst(docs, *pat, *value, state)? + } + mir::MirInst::Assign { target, value, .. } => { + self.emit_assign_inst(docs, *target, *value, state)? + } + mir::MirInst::AugAssign { + target, value, op, .. + } => self.emit_augassign_inst(docs, *target, *value, *op, state)?, + mir::MirInst::Eval { value, .. } => self.emit_eval_inst(docs, *value, state)?, + mir::MirInst::EvalExpr { + expr, + value, + bind_value, + } => self.emit_eval_expr_inst(docs, *expr, *value, *bind_value, state)?, + mir::MirInst::IntrinsicStmt { op, args, .. } => { + self.emit_intrinsic_inst(docs, *op, args, state)? + } + } + Ok(()) + } + + /// Lowers a `let` instruction, allocating or updating the binding. + /// + /// * `docs` - Output vector receiving the generated Yul statement. + /// * `pat` - Pattern representing the binding name. + /// * `value` - Optional initializer value. + /// * `state` - Binding table used to store/reuse the lowered slot. + /// + /// Returns `Ok(())` when the binding has been populated. + fn emit_let_inst( + &mut self, + docs: &mut Vec, + pat: PatId, + value: Option, + state: &mut BlockState, + ) -> Result<(), YulError> { + let binding = self.pattern_ident(pat)?; + let existing = state.binding(&binding); + let value = match value { + Some(val) => self.lower_value(val, state)?, + None => "0".into(), + }; + if let Some(name) = existing { + docs.push(YulDoc::line(format!("{name} := {value}"))); + } else { + let temp = state.alloc_local(); + state.insert_binding(binding.clone(), temp.clone()); + docs.push(YulDoc::line(format!("let {temp} := {value}"))); + } + Ok(()) + } + + /// Lowers a plain assignment (`x = y`) into Yul. + /// + /// * `docs` - Collection where the resulting assignment is appended. + /// * `target` - Assignment LHS expression. + /// * `value` - MIR value providing the RHS. + /// * `state` - Block state storing active Yul bindings. + /// + /// Returns `Ok(())` once the assignment has been recorded. + fn emit_assign_inst( + &mut self, + docs: &mut Vec, + target: ExprId, + value: ValueId, + state: &mut BlockState, + ) -> Result<(), YulError> { + let binding = self.path_from_expr(target)?; + let yul_name = state + .binding(&binding) + .ok_or_else(|| YulError::Unsupported("assignment to unknown binding".into()))?; + let value_expr = self.lower_value(value, state)?; + docs.push(YulDoc::line(format!("{yul_name} := {value_expr}"))); + Ok(()) + } + + /// Emits an augmented assignment (`+=`, `-=`, …) into the corresponding Yul op. + /// + /// * `docs` - Collector for the generated statement. + /// * `target` - Assignment LHS expression. + /// * `value` - RHS operand. + /// * `op` - Arithmetic operator used in the augmentation. + /// * `state` - Block state providing access to binding slots. + /// + /// Returns `Ok(())` when the augmented assignment has been emitted. + fn emit_augassign_inst( + &mut self, + docs: &mut Vec, + target: ExprId, + value: ValueId, + op: ArithBinOp, + state: &mut BlockState, + ) -> Result<(), YulError> { + let binding = self.path_from_expr(target)?; + let yul_name = state + .binding(&binding) + .ok_or_else(|| YulError::Unsupported("assignment to unknown binding".into()))?; + let rhs = self.lower_value(value, state)?; + let assignment = match op { + ArithBinOp::Add => format!("add({yul_name}, {rhs})"), + ArithBinOp::Sub => format!("sub({yul_name}, {rhs})"), + ArithBinOp::Mul => format!("mul({yul_name}, {rhs})"), + ArithBinOp::Div => format!("div({yul_name}, {rhs})"), + ArithBinOp::Rem => format!("mod({yul_name}, {rhs})"), + ArithBinOp::Pow => format!("exp({yul_name}, {rhs})"), + ArithBinOp::LShift => format!("shl({rhs}, {yul_name})"), + ArithBinOp::RShift => format!("shr({rhs}, {yul_name})"), + ArithBinOp::BitAnd => format!("and({yul_name}, {rhs})"), + ArithBinOp::BitOr => format!("or({yul_name}, {rhs})"), + ArithBinOp::BitXor => format!("xor({yul_name}, {rhs})"), + }; + docs.push(YulDoc::line(format!("{yul_name} := {assignment}"))); + Ok(()) + } + + /// Emits an expression statement whose value is not reused. + /// + /// * `docs` - Accumulator for any generated docs. + /// * `value` - MIR value used for the expression statement. + /// * `state` - Block state containing active bindings. + /// + /// Refrains from re-emitting expressions consumed elsewhere and returns + /// `Ok(())` after optionally pushing a doc. + fn emit_eval_inst( + &mut self, + docs: &mut Vec, + value: ValueId, + state: &mut BlockState, + ) -> Result<(), YulError> { + if self.value_use_counts[value.index()] == 1 + && let Some(doc) = self.render_eval(value, state)? + { + docs.push(doc); + } + Ok(()) + } + + /// Emits evaluation logic for expressions that optionally bind to a temporary. + /// + /// * `docs` - Output buffer for the emitted stmt or binding. + /// * `expr` - Source expression ID. + /// * `value` - MIR value produced for that expression. + /// * `bind_value` - Whether the result should be bound for reuse. + /// * `state` - Mutable per-block scope that owns temporaries. + /// + /// Returns `Ok(())` when the expression has been handled. + fn emit_eval_expr_inst( + &mut self, + docs: &mut Vec, + expr: ExprId, + value: ValueId, + bind_value: bool, + state: &mut BlockState, + ) -> Result<(), YulError> { + let lowered = self.lower_value(value, state)?; + if bind_value { + let temp = state.alloc_local(); + self.expr_temps.insert(expr, temp.clone()); + docs.push(YulDoc::line(format!("let {temp} := {lowered}"))); + } else { + let value_data = self.mir_func.body.value(value); + if matches!(value_data.origin, ValueOrigin::Call(..)) { + docs.push(YulDoc::line(format!("pop({lowered})"))); + } else { + docs.push(YulDoc::line(lowered)); + } + } + Ok(()) + } + + /// Emits Yul for a statement-only intrinsic (e.g. `mstore`). + /// + /// * `docs` - Collection to append the statement to when one is emitted. + /// * `op` - Intrinsic opcode. + /// * `args` - MIR value arguments. + /// * `state` - Block-local bindings used to lower the arguments. + /// + /// Returns `Ok(())` once the intrinsic (if applicable) has been appended. + fn emit_intrinsic_inst( + &mut self, + docs: &mut Vec, + op: IntrinsicOp, + args: &[ValueId], + state: &mut BlockState, + ) -> Result<(), YulError> { + let intr = IntrinsicValue { + op, + args: args.to_vec(), + }; + if let Some(doc) = self.lower_intrinsic_stmt(&intr, state)? { + docs.push(doc); + } + Ok(()) + } + + /// Emits statements for expression statements, returning a doc when work was done. + /// + /// * `value_id` - MIR value representing the expression. + /// * `state` - Block state containing active bindings. + /// + /// Returns a doc describing the evaluation side effects, if any. + pub(super) fn render_eval( + &mut self, + value_id: ValueId, + state: &mut BlockState, + ) -> Result, YulError> { + let value = self.mir_func.body.value(value_id); + match &value.origin { + ValueOrigin::Intrinsic(intr) => self.lower_intrinsic_stmt(intr, state), + ValueOrigin::Call(call) => { + let call_expr = self.lower_call_value(call, state)?; + Ok(Some(YulDoc::line(format!("pop({call_expr})")))) + } + _ => Ok(None), + } + } + + /// Handles `return intrinsic::(...)` for void intrinsics by emitting the + /// side effect plus a `ret := 0`. + /// + /// * `value_id` - MIR value representing the intrinsic call. + /// * `docs` - Yul statement accumulator. + /// * `state` - Immutable view over block bindings to resolve arguments. + /// + /// Returns `Ok(true)` when the intrinsic produced a replacement return statement. + pub(super) fn emit_intrinsic_return( + &mut self, + value_id: ValueId, + docs: &mut Vec, + state: &BlockState, + ) -> Result { + let value = self.mir_func.body.value(value_id); + if let ValueOrigin::Intrinsic(intr) = &value.origin + && !intr.op.returns_value() + { + if let Some(doc) = self.lower_intrinsic_stmt(intr, state)? { + docs.push(doc); + } + if matches!(intr.op, IntrinsicOp::ReturnData) { + return Ok(true); + } + docs.push(YulDoc::line("ret := 0")); + return Ok(true); + } + Ok(false) + } + + /// Converts intrinsic value-producing operations (`mload`/`sload`) into Yul. + /// + /// * `intr` - Intrinsic call metadata containing opcode and arguments. + /// * `state` - Read-only block state needed to lower arguments. + /// + /// Returns the Yul expression describing the intrinsic invocation. + pub(super) fn lower_intrinsic_value( + &self, + intr: &IntrinsicValue, + state: &BlockState, + ) -> Result { + if !intr.op.returns_value() { + return Err(YulError::Unsupported( + "intrinsic does not yield a value".into(), + )); + } + let args = self.lower_intrinsic_args(intr, state)?; + self.expect_intrinsic_arity(intr.op, &args, 1)?; + Ok(format!("{}({})", self.intrinsic_name(intr.op), args[0])) + } + + /// Converts intrinsic statement operations (`mstore`, …) into Yul. + /// + /// * `intr` - Intrinsic call metadata describing the opcode and args. + /// * `state` - Block state needed to lower the intrinsic operands. + /// + /// Returns the emitted doc when the intrinsic performs work. + pub(super) fn lower_intrinsic_stmt( + &self, + intr: &IntrinsicValue, + state: &BlockState, + ) -> Result, YulError> { + if intr.op.returns_value() { + return Ok(None); + } + let args = self.lower_intrinsic_args(intr, state)?; + self.expect_intrinsic_arity(intr.op, &args, 2)?; + let line = match intr.op { + IntrinsicOp::Mstore => format!("mstore({}, {})", args[0], args[1]), + IntrinsicOp::Mstore8 => format!("mstore8({}, {})", args[0], args[1]), + IntrinsicOp::Sstore => format!("sstore({}, {})", args[0], args[1]), + IntrinsicOp::ReturnData => format!("return({}, {})", args[0], args[1]), + _ => unreachable!(), + }; + Ok(Some(YulDoc::line(line))) + } + + /// Lowers all intrinsic arguments into Yul expressions. + /// + /// * `intr` - Intrinsic call describing the operands. + /// * `state` - Block state used to lower each operand. + /// + /// Returns the lowered argument list in call order. + fn lower_intrinsic_args( + &self, + intr: &IntrinsicValue, + state: &BlockState, + ) -> Result, YulError> { + intr.args + .iter() + .map(|arg| self.lower_value(*arg, state)) + .collect() + } + + /// Ensures the intrinsic received the expected number of operands. + /// + /// * `op` - Intrinsic opcode being validated. + /// * `args` - Lowered operand list. + /// * `expected` - Required arity for the opcode. + /// + /// Returns `Ok(())` when the arity matches, otherwise an unsupported error. + fn expect_intrinsic_arity( + &self, + op: IntrinsicOp, + args: &[String], + expected: usize, + ) -> Result<(), YulError> { + if args.len() == expected { + Ok(()) + } else { + Err(YulError::Unsupported(format!( + "intrinsic `{}` expects {expected} arguments, got {}", + self.intrinsic_name(op), + args.len() + ))) + } + } + + /// Returns the Yul builtin name for an intrinsic opcode. + /// + /// * `op` - Intrinsic opcode to translate. + /// + /// Returns the canonical Yul mnemonic corresponding to the opcode. + fn intrinsic_name(&self, op: IntrinsicOp) -> &'static str { + match op { + IntrinsicOp::Mload => "mload", + IntrinsicOp::Calldataload => "calldataload", + IntrinsicOp::Mstore => "mstore", + IntrinsicOp::Mstore8 => "mstore8", + IntrinsicOp::Sload => "sload", + IntrinsicOp::Sstore => "sstore", + IntrinsicOp::ReturnData => "return", + } + } +} diff --git a/crates/codegen/src/yul/emitter/util.rs b/crates/codegen/src/yul/emitter/util.rs new file mode 100644 index 000000000..63eea0321 --- /dev/null +++ b/crates/codegen/src/yul/emitter/util.rs @@ -0,0 +1,68 @@ +//! Shared utility helpers used across the Yul emitter modules. + +use driver::DriverDataBase; +use hir::hir_def::Func; + +use super::YulError; + +/// Returns the display name of a function or `` if one does not exist. +/// +/// * `func` - HIR function to name. +/// +/// Returns the display string used for diagnostics and Yul names. +pub(super) fn function_name(db: &DriverDataBase, func: Func<'_>) -> String { + func.name(db) + .to_opt() + .map(|id| id.data(db).to_string()) + .unwrap_or_else(|| "".into()) +} + +/// Converts usages of cast shims into their lone argument so we don't emit fake calls. +/// +/// * `name` - Function identifier for the shim. +/// * `args` - Already-lowered argument expressions. +/// +/// Returns the collapsed argument when `name` is a shim, otherwise `None`. +pub(super) fn try_collapse_cast_shim( + name: &str, + args: &[String], +) -> Result, YulError> { + if !is_cast_shim(name) { + return Ok(None); + } + debug_assert_eq!( + args.len(), + 1, + "cast shims are expected to take a single argument" + ); + let arg = args + .first() + .cloned() + .ok_or_else(|| YulError::Unsupported("cast shim missing argument".into()))?; + Ok(Some(arg)) +} + +/// Returns `true` when `name` matches one of the temporary casting shims +/// (`__{src}_as_{dst}`) used while the `as` syntax is unavailable. +fn is_cast_shim(name: &str) -> bool { + cast_shim_parts(name).is_some() +} + +/// Validates that a name follows the `__{src}_as_{dst}` convention and returns the parts. +fn cast_shim_parts(name: &str) -> Option<(&str, &str)> { + let stripped = name.strip_prefix("__")?; + let (src, dst) = stripped.split_once("_as_")?; + if src.is_empty() || dst.is_empty() { + return None; + } + if !is_cast_ident(src) || !is_cast_ident(dst) { + return None; + } + Some((src, dst)) +} + +/// Validates that a substring of a shim name matches the allowed identifier schema. +fn is_cast_ident(part: &str) -> bool { + part.chars() + .all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit()) +} diff --git a/crates/codegen/src/yul/mod.rs b/crates/codegen/src/yul/mod.rs index bcee03439..15f81919e 100644 --- a/crates/codegen/src/yul/mod.rs +++ b/crates/codegen/src/yul/mod.rs @@ -3,5 +3,5 @@ mod emitter; mod errors; mod state; -pub use emitter::emit_module_yul; +pub use emitter::{EmitModuleError, emit_module_yul}; pub use errors::YulError; diff --git a/crates/codegen/src/yul/state.rs b/crates/codegen/src/yul/state.rs index 71f735c8a..150ded0ff 100644 --- a/crates/codegen/src/yul/state.rs +++ b/crates/codegen/src/yul/state.rs @@ -1,10 +1,13 @@ +use std::rc::Rc; + use rustc_hash::FxHashMap; +use std::cell::Cell; /// Tracks the mapping between Fe bindings and their Yul local names within a block. #[derive(Clone)] pub(super) struct BlockState { locals: FxHashMap, - next_local: usize, + next_local: Rc>, } impl BlockState { @@ -12,14 +15,15 @@ impl BlockState { pub(super) fn new() -> Self { Self { locals: FxHashMap::default(), - next_local: 0, + next_local: Rc::new(Cell::new(0)), } } /// Allocates a new temporary Yul variable name. pub(super) fn alloc_local(&mut self) -> String { - let name = format!("v{}", self.next_local); - self.next_local += 1; + let current = self.next_local.get(); + let name = format!("v{}", current); + self.next_local.set(current + 1); name } diff --git a/crates/codegen/tests/fixtures/alloc.snap b/crates/codegen/tests/fixtures/alloc.snap index 259bc13a5..a061b1640 100644 --- a/crates/codegen/tests/fixtures/alloc.snap +++ b/crates/codegen/tests/fixtures/alloc.snap @@ -1,31 +1,25 @@ --- source: crates/codegen/tests/yul.rs -assertion_line: 38 +assertion_line: 31 expression: output input_file: tests/fixtures/alloc.fe --- -{ - function bump(size) -> ret { - ret := alloc(size) - } +function bump(size) -> ret { + ret := alloc(size) } -{ - function bump_const() -> ret { - ret := alloc(64) - } +function bump_const() -> ret { + ret := alloc(64) } -{ - function alloc(size) -> ret { - let v0 := mload(64) - let v1 := eq(v0, 0) - if v1 { - v0 := 128 - mstore(64, add(v0, size)) - ret := v0 - } - if iszero(v1) { - mstore(64, add(v0, size)) - ret := v0 - } +function alloc(size) -> ret { + let v0 := mload(64) + let v1 := eq(v0, 0) + if v1 { + v0 := 128 + mstore(64, add(v0, size)) + ret := v0 + } + if iszero(v1) { + mstore(64, add(v0, size)) + ret := v0 } } diff --git a/crates/codegen/tests/fixtures/arg_bindings.snap b/crates/codegen/tests/fixtures/arg_bindings.snap index 69cb6599e..7237b263a 100644 --- a/crates/codegen/tests/fixtures/arg_bindings.snap +++ b/crates/codegen/tests/fixtures/arg_bindings.snap @@ -1,11 +1,10 @@ --- source: crates/codegen/tests/yul.rs +assertion_line: 31 expression: output input_file: tests/fixtures/arg_bindings.fe --- -{ - function arg_bindings(x, y) -> ret { - let v0 := add(x, y) - ret := v0 - } +function arg_bindings(x, y) -> ret { + let v0 := add(x, y) + ret := v0 } diff --git a/crates/codegen/tests/fixtures/aug_assign.snap b/crates/codegen/tests/fixtures/aug_assign.snap index 6bd89ff8b..1b33da5f4 100644 --- a/crates/codegen/tests/fixtures/aug_assign.snap +++ b/crates/codegen/tests/fixtures/aug_assign.snap @@ -1,14 +1,12 @@ --- source: crates/codegen/tests/yul.rs -assertion_line: 38 +assertion_line: 31 expression: output input_file: tests/fixtures/aug_assign.fe --- -{ - function aug_assign(x, y) -> ret { - let v0 := x - v0 := add(v0, y) - v0 := mul(v0, 2) - ret := div(v0, 2) - } +function aug_assign(x, y) -> ret { + let v0 := x + v0 := add(v0, y) + v0 := mul(v0, 2) + ret := div(v0, 2) } diff --git a/crates/codegen/tests/fixtures/aug_assign_bit_ops.snap b/crates/codegen/tests/fixtures/aug_assign_bit_ops.snap index 994039296..0c91a0b6e 100644 --- a/crates/codegen/tests/fixtures/aug_assign_bit_ops.snap +++ b/crates/codegen/tests/fixtures/aug_assign_bit_ops.snap @@ -1,18 +1,16 @@ --- source: crates/codegen/tests/yul.rs -assertion_line: 38 +assertion_line: 31 expression: output input_file: tests/fixtures/aug_assign_bit_ops.fe --- -{ - function aug_assign_bit_ops(x, y) -> ret { - let v0 := x - v0 := exp(v0, y) - v0 := shl(3, v0) - v0 := shr(1, v0) - v0 := and(v0, 255) - v0 := or(v0, 1) - v0 := xor(v0, 2) - ret := v0 - } +function aug_assign_bit_ops(x, y) -> ret { + let v0 := x + v0 := exp(v0, y) + v0 := shl(3, v0) + v0 := shr(1, v0) + v0 := and(v0, 255) + v0 := or(v0, 1) + v0 := xor(v0, 2) + ret := v0 } diff --git a/crates/codegen/tests/fixtures/bit_and.snap b/crates/codegen/tests/fixtures/bit_and.snap index 7905937a9..3c52ecd15 100644 --- a/crates/codegen/tests/fixtures/bit_and.snap +++ b/crates/codegen/tests/fixtures/bit_and.snap @@ -1,11 +1,9 @@ --- source: crates/codegen/tests/yul.rs -assertion_line: 38 +assertion_line: 31 expression: output -input_file: tests/fixtures/bitwise_unsupported.fe +input_file: tests/fixtures/bit_and.fe --- -{ - function bit_and() -> ret { - ret := and(1, 2) - } +function bit_and() -> ret { + ret := and(1, 2) } diff --git a/crates/codegen/tests/fixtures/bit_ops.snap b/crates/codegen/tests/fixtures/bit_ops.snap index 906cffdac..58086d178 100644 --- a/crates/codegen/tests/fixtures/bit_ops.snap +++ b/crates/codegen/tests/fixtures/bit_ops.snap @@ -1,21 +1,17 @@ --- source: crates/codegen/tests/yul.rs -assertion_line: 38 +assertion_line: 31 expression: output input_file: tests/fixtures/bit_ops.fe --- -{ - function bit_ops(x, y) -> ret { - let v0 := and(x, y) - let v1 := or(x, y) - let v2 := xor(x, y) - let v3 := shl(8, x) - let v4 := shr(2, x) - ret := tuple(v0, v1, v2, v3, v4) - } +function bit_ops(x, y) -> ret { + let v0 := and(x, y) + let v1 := or(x, y) + let v2 := xor(x, y) + let v3 := shl(8, x) + let v4 := shr(2, x) + ret := tuple(v0, v1, v2, v3, v4) } -{ - function pow_op(x, y) -> ret { - ret := exp(x, y) - } +function pow_op(x, y) -> ret { + ret := exp(x, y) } diff --git a/crates/codegen/tests/fixtures/bitnot.snap b/crates/codegen/tests/fixtures/bitnot.snap index 0a017ecec..a8fe9ba00 100644 --- a/crates/codegen/tests/fixtures/bitnot.snap +++ b/crates/codegen/tests/fixtures/bitnot.snap @@ -1,11 +1,9 @@ --- source: crates/codegen/tests/yul.rs -assertion_line: 38 +assertion_line: 31 expression: output input_file: tests/fixtures/bitnot.fe --- -{ - function bit_not(x) -> ret { - ret := not(x) - } +function bit_not(x) -> ret { + ret := not(x) } diff --git a/crates/codegen/tests/fixtures/block_expr.snap b/crates/codegen/tests/fixtures/block_expr.snap index ec4026ab9..a3ae8ad43 100644 --- a/crates/codegen/tests/fixtures/block_expr.snap +++ b/crates/codegen/tests/fixtures/block_expr.snap @@ -1,10 +1,9 @@ --- source: crates/codegen/tests/yul.rs +assertion_line: 31 expression: output input_file: tests/fixtures/block_expr.fe --- -{ - function block_expr() -> ret { - ret := add(1, 2) - } +function block_expr() -> ret { + ret := add(1, 2) } diff --git a/crates/codegen/tests/fixtures/bool_literal.snap b/crates/codegen/tests/fixtures/bool_literal.snap index c9a7310ce..0860cec27 100644 --- a/crates/codegen/tests/fixtures/bool_literal.snap +++ b/crates/codegen/tests/fixtures/bool_literal.snap @@ -1,10 +1,9 @@ --- source: crates/codegen/tests/yul.rs +assertion_line: 31 expression: output input_file: tests/fixtures/bool_literal.fe --- -{ - function bool_literal() -> ret { - ret := 1 - } +function bool_literal() -> ret { + ret := 1 } diff --git a/crates/codegen/tests/fixtures/comparison_ops.snap b/crates/codegen/tests/fixtures/comparison_ops.snap index b258f47c7..4419eb153 100644 --- a/crates/codegen/tests/fixtures/comparison_ops.snap +++ b/crates/codegen/tests/fixtures/comparison_ops.snap @@ -1,10 +1,9 @@ --- source: crates/codegen/tests/yul.rs +assertion_line: 31 expression: output input_file: tests/fixtures/comparison_ops.fe --- -{ - function comparison_ops() -> ret { - ret := tuple(eq(1, 1), iszero(eq(1, 2)), lt(1, 2), iszero(gt(2, 2)), gt(3, 2), iszero(lt(3, 3))) - } +function comparison_ops() -> ret { + ret := tuple(eq(1, 1), iszero(eq(1, 2)), lt(1, 2), iszero(gt(2, 2)), gt(3, 2), iszero(lt(3, 3))) } diff --git a/crates/codegen/tests/fixtures/contract_dispatch.fe b/crates/codegen/tests/fixtures/contract_dispatch.fe new file mode 100644 index 000000000..34becc78a --- /dev/null +++ b/crates/codegen/tests/fixtures/contract_dispatch.fe @@ -0,0 +1,13 @@ +use core::Dispatcher + +contract MinimalDispatcher {} + +fn emit_code() -> u64 { + 1 +} + +impl Dispatcher for MinimalDispatcher { + fn dispatch() { + let _value: u64 = emit_code() + } +} diff --git a/crates/codegen/tests/fixtures/contract_dispatch.snap b/crates/codegen/tests/fixtures/contract_dispatch.snap new file mode 100644 index 000000000..1f63b5c91 --- /dev/null +++ b/crates/codegen/tests/fixtures/contract_dispatch.snap @@ -0,0 +1,27 @@ +--- +source: crates/codegen/tests/yul.rs +assertion_line: 31 +expression: output +input_file: tests/fixtures/contract_dispatch.fe +--- +object "MinimalDispatcher" { + code { + datacopy(0, dataoffset("runtime"), datasize("runtime")) + return(0, datasize("runtime")) + } + + object "runtime" { + code { + function emit_code() -> ret { + ret := 1 + } + function dispatch() -> ret { + let v0 := emit_code() + ret := 0 + } + + pop(dispatch()) + stop() + } + } +} diff --git a/crates/codegen/tests/fixtures/full_contract.fe b/crates/codegen/tests/fixtures/full_contract.fe new file mode 100644 index 000000000..df0b8b4d8 --- /dev/null +++ b/crates/codegen/tests/fixtures/full_contract.fe @@ -0,0 +1,48 @@ +use core::{Dispatcher, calldataload, mstore, return_data} + +struct Point { x: u256, y: u256 } +struct Square { side: u256 } + +impl Point { + fn area(self) -> u256 { + self.x * self.y + } +} + +impl Square { + fn area(self) -> u256 { + let side = self.side + side * side + } +} + +contract UselessSecondContract {} + +contract ShapeDispatcher {} + +fn abi_encode(value: u256) { + let ptr: u256 = 0 + mstore(ptr, value) + return_data(ptr, 32) +} + +impl Dispatcher for ShapeDispatcher { + fn dispatch() { + let selector = calldataload(0) >> 224 + if selector == 0x090251bf { + let x = calldataload(4) + let y = calldataload(36) + let p = Point { x: x, y: y } + let value = p.area() + abi_encode(value) + } + if selector == 0x7b292909 { + let side = calldataload(4) + let s = Square { side: side } + let value = s.area() + abi_encode(value) + } + + return_data(0, 0) + } +} \ No newline at end of file diff --git a/crates/codegen/tests/fixtures/full_contract.snap b/crates/codegen/tests/fixtures/full_contract.snap new file mode 100644 index 000000000..7b7d54f2b --- /dev/null +++ b/crates/codegen/tests/fixtures/full_contract.snap @@ -0,0 +1,120 @@ +--- +source: crates/codegen/tests/yul.rs +assertion_line: 31 +expression: output +input_file: tests/fixtures/full_contract.fe +--- +object "ShapeDispatcher" { + code { + datacopy(0, dataoffset("runtime"), datasize("runtime")) + return(0, datasize("runtime")) + } + + object "runtime" { + code { + function point_area(self) -> ret { + ret := mul(get_field__Point_u256__78e69939a7cc685d(self, 0, 0), get_field__Point_u256__78e69939a7cc685d(self, 0, 32)) + } + function square_area(self) -> ret { + let v0 := get_field__Point_u256__78e69939a7cc685d(self, 0, 0) + ret := mul(v0, v0) + } + function abi_encode(value) -> ret { + let v0 := 0 + mstore(v0, value) + return(v0, 32) + } + function dispatch() -> ret { + let v0 := shr(224, calldataload(0)) + let v1 := eq(v0, 151146943) + if v1 { + let v2 := calldataload(4) + let v3 := calldataload(36) + let v4 := alloc(64) + pop(store_field__deduped(v4, 0, 0, v2)) + pop(store_field__deduped(v4, 0, 32, v3)) + let v5 := v4 + let v6 := point_area(v5) + pop(abi_encode(v6)) + let v7 := eq(v0, 2066295049) + if v7 { + let v8 := calldataload(4) + let v9 := alloc(32) + pop(store_field__deduped(v9, 0, 0, v8)) + let v10 := v9 + v6 := square_area(v10) + pop(abi_encode(v6)) + return(0, 0) + } + if iszero(v7) { + return(0, 0) + } + } + if iszero(v1) { + let v11 := eq(v0, 2066295049) + if v11 { + let v12 := calldataload(4) + let v13 := alloc(32) + pop(store_field__deduped(v13, 0, 0, v12)) + let v14 := v13 + let v15 := square_area(v14) + pop(abi_encode(v15)) + return(0, 0) + } + if iszero(v11) { + return(0, 0) + } + } + } + function get_field__Point_u256__78e69939a7cc685d(addr, space, offset) -> ret { + let v1 := add(addr, offset) + let v0 := 0 + switch space + case 0 { + v0 := mload(v1) + } + case 1 { + v0 := sload(v1) + } + default { + } + let v2 := v0 + ret := to_word__deduped(v2) + } + function alloc(size) -> ret { + let v0 := mload(64) + let v1 := eq(v0, 0) + if v1 { + v0 := 128 + mstore(64, add(v0, size)) + ret := v0 + } + if iszero(v1) { + mstore(64, add(v0, size)) + ret := v0 + } + } + function store_field__deduped(addr, space, offset, value) -> ret { + let v0 := add(addr, offset) + switch space + case 0 { + mstore(v0, to_word__deduped(value)) + ret := 0 + } + case 1 { + sstore(v0, to_word__deduped(value)) + ret := 0 + } + default { + } + ret := 0 + } + function to_word__deduped(self) -> ret { + ret := self + } + + pop(dispatch()) + stop() + } + } +} diff --git a/crates/codegen/tests/fixtures/function_call.snap b/crates/codegen/tests/fixtures/function_call.snap index 47bc15cbc..545c51dad 100644 --- a/crates/codegen/tests/fixtures/function_call.snap +++ b/crates/codegen/tests/fixtures/function_call.snap @@ -1,15 +1,12 @@ --- source: crates/codegen/tests/yul.rs +assertion_line: 31 expression: output input_file: tests/fixtures/function_call.fe --- -{ - function add_one(x) -> ret { - ret := add(x, 1) - } +function add_one(x) -> ret { + ret := add(x, 1) } -{ - function call_add_one() -> ret { - ret := add_one(5) - } +function call_add_one() -> ret { + ret := add_one(5) } diff --git a/crates/codegen/tests/fixtures/generic_identity.snap b/crates/codegen/tests/fixtures/generic_identity.snap index c462cab45..f3c580a8f 100644 --- a/crates/codegen/tests/fixtures/generic_identity.snap +++ b/crates/codegen/tests/fixtures/generic_identity.snap @@ -1,28 +1,20 @@ --- source: crates/codegen/tests/yul.rs -assertion_line: 38 +assertion_line: 31 expression: output input_file: tests/fixtures/generic_identity.fe --- -{ - function call_identity_u32() -> ret { - let v0 := identity__u32__20aa0c10687491ad(7) - ret := v0 - } +function call_identity_u32() -> ret { + let v0 := identity__u32__20aa0c10687491ad(7) + ret := v0 } -{ - function call_identity_bool() -> ret { - let v0 := identity__bool__947c0c03c59c6f07(1) - ret := v0 - } +function call_identity_bool() -> ret { + let v0 := identity__bool__947c0c03c59c6f07(1) + ret := v0 } -{ - function identity__u32__20aa0c10687491ad(value) -> ret { - ret := value - } +function identity__u32__20aa0c10687491ad(value) -> ret { + ret := value } -{ - function identity__bool__947c0c03c59c6f07(value) -> ret { - ret := value - } +function identity__bool__947c0c03c59c6f07(value) -> ret { + ret := value } diff --git a/crates/codegen/tests/fixtures/if_else.snap b/crates/codegen/tests/fixtures/if_else.snap index d5e31a1c1..a694b6dac 100644 --- a/crates/codegen/tests/fixtures/if_else.snap +++ b/crates/codegen/tests/fixtures/if_else.snap @@ -1,17 +1,16 @@ --- source: crates/codegen/tests/yul.rs +assertion_line: 31 expression: output input_file: tests/fixtures/if_else.fe --- -{ - function if_else(cond) -> ret { - let v0 := 0 - if cond { - v0 := 1 - } - if iszero(cond) { - v0 := 2 - } - ret := v0 +function if_else(cond) -> ret { + let v0 := 0 + if cond { + v0 := 1 } + if iszero(cond) { + v0 := 2 + } + ret := v0 } diff --git a/crates/codegen/tests/fixtures/intrinsic_ops.fe b/crates/codegen/tests/fixtures/intrinsic_ops.fe index d296737bc..d51804d7b 100644 --- a/crates/codegen/tests/fixtures/intrinsic_ops.fe +++ b/crates/codegen/tests/fixtures/intrinsic_ops.fe @@ -1,5 +1,4 @@ -// TODO: Should be core::intrinsic::mload etc. -use core::{mload, mstore, mstore8, sload, sstore} +use core::{calldataload, mload, mstore, mstore8, return_data, sload, sstore} pub fn load_word(ptr: u256) -> u256 { mload(ptr) @@ -20,3 +19,11 @@ pub fn load_slot(slot: u256) -> u256 { pub fn store_slot(slot: u256, value: u256) { sstore(slot, value) } + +pub fn load_calldata(offset: u256) -> u256 { + calldataload(offset) +} + +pub fn finish(ptr: u256, len: u256) { + return_data(ptr, len) +} diff --git a/crates/codegen/tests/fixtures/intrinsic_ops.snap b/crates/codegen/tests/fixtures/intrinsic_ops.snap index 30222ba1f..7fba90cb8 100644 --- a/crates/codegen/tests/fixtures/intrinsic_ops.snap +++ b/crates/codegen/tests/fixtures/intrinsic_ops.snap @@ -1,34 +1,30 @@ --- source: crates/codegen/tests/yul.rs -assertion_line: 38 +assertion_line: 31 expression: output input_file: tests/fixtures/intrinsic_ops.fe --- -{ - function load_word(ptr) -> ret { - ret := mload(ptr) - } +function load_word(ptr) -> ret { + ret := mload(ptr) } -{ - function store_word(ptr, value) -> ret { - mstore(ptr, value) - ret := 0 - } +function store_word(ptr, value) -> ret { + mstore(ptr, value) + ret := 0 } -{ - function store_byte(ptr, value) -> ret { - mstore8(ptr, value) - ret := 0 - } +function store_byte(ptr, value) -> ret { + mstore8(ptr, value) + ret := 0 } -{ - function load_slot(slot) -> ret { - ret := sload(slot) - } +function load_slot(slot) -> ret { + ret := sload(slot) } -{ - function store_slot(slot, value) -> ret { - sstore(slot, value) - ret := 0 - } +function store_slot(slot, value) -> ret { + sstore(slot, value) + ret := 0 +} +function load_calldata(offset) -> ret { + ret := calldataload(offset) +} +function finish(ptr, len) -> ret { + return(ptr, len) } diff --git a/crates/codegen/tests/fixtures/literal_add.snap b/crates/codegen/tests/fixtures/literal_add.snap index 0bfbc51a3..eef35e8a6 100644 --- a/crates/codegen/tests/fixtures/literal_add.snap +++ b/crates/codegen/tests/fixtures/literal_add.snap @@ -1,10 +1,9 @@ --- source: crates/codegen/tests/yul.rs +assertion_line: 31 expression: output input_file: tests/fixtures/literal_add.fe --- -{ - function literal_add() -> ret { - ret := add(1, 2) - } +function literal_add() -> ret { + ret := add(1, 2) } diff --git a/crates/codegen/tests/fixtures/literal_sub.snap b/crates/codegen/tests/fixtures/literal_sub.snap index dfc58dfca..79b2adeb4 100644 --- a/crates/codegen/tests/fixtures/literal_sub.snap +++ b/crates/codegen/tests/fixtures/literal_sub.snap @@ -1,11 +1,9 @@ --- source: crates/codegen/tests/yul.rs -assertion_line: 33 +assertion_line: 31 expression: output input_file: tests/fixtures/literal_sub.fe --- -{ - function literal_sub() -> ret { - ret := sub(1, 2) - } +function literal_sub() -> ret { + ret := sub(1, 2) } diff --git a/crates/codegen/tests/fixtures/local_bindings.snap b/crates/codegen/tests/fixtures/local_bindings.snap index 5be3ea176..fc6c4613b 100644 --- a/crates/codegen/tests/fixtures/local_bindings.snap +++ b/crates/codegen/tests/fixtures/local_bindings.snap @@ -1,12 +1,11 @@ --- source: crates/codegen/tests/yul.rs +assertion_line: 31 expression: output input_file: tests/fixtures/local_bindings.fe --- -{ - function local_bindings() -> ret { - let v0 := add(1, 2) - let v1 := 3 - ret := v1 - } +function local_bindings() -> ret { + let v0 := add(1, 2) + let v1 := 3 + ret := v1 } diff --git a/crates/codegen/tests/fixtures/logical_ops.snap b/crates/codegen/tests/fixtures/logical_ops.snap index 57f75a2b0..8f71e2735 100644 --- a/crates/codegen/tests/fixtures/logical_ops.snap +++ b/crates/codegen/tests/fixtures/logical_ops.snap @@ -1,10 +1,9 @@ --- source: crates/codegen/tests/yul.rs +assertion_line: 31 expression: output input_file: tests/fixtures/logical_ops.fe --- -{ - function logical_ops() -> ret { - ret := and(1, 0) - } +function logical_ops() -> ret { + ret := and(1, 0) } diff --git a/crates/codegen/tests/fixtures/match_enum.snap b/crates/codegen/tests/fixtures/match_enum.snap index ad68cafcc..2eec0bb79 100644 --- a/crates/codegen/tests/fixtures/match_enum.snap +++ b/crates/codegen/tests/fixtures/match_enum.snap @@ -1,25 +1,23 @@ --- source: crates/codegen/tests/yul.rs -assertion_line: 38 +assertion_line: 31 expression: output input_file: tests/fixtures/match_enum.fe --- -{ - function match_enum(e) -> ret { - let v0 := 0 - switch e - case 0 { - v0 := 1 - } - case 1 { - v0 := 2 - } - case 2 { - v0 := 3 - } - default { - v0 := 4 - } - ret := v0 - } +function match_enum(e) -> ret { + let v0 := 0 + switch e + case 0 { + v0 := 1 + } + case 1 { + v0 := 2 + } + case 2 { + v0 := 3 + } + default { + v0 := 4 + } + ret := v0 } diff --git a/crates/codegen/tests/fixtures/match_literal.snap b/crates/codegen/tests/fixtures/match_literal.snap index a04e1235d..f18a3eb09 100644 --- a/crates/codegen/tests/fixtures/match_literal.snap +++ b/crates/codegen/tests/fixtures/match_literal.snap @@ -1,210 +1,185 @@ --- source: crates/codegen/tests/yul.rs +assertion_line: 31 expression: output input_file: tests/fixtures/match_literal.fe --- -{ - function match_bool(e) -> ret { - let v0 := 0 - switch e - case 1 { - v0 := 1 - } - default { - v0 := 2 - } - ret := v0 - } +function match_bool(e) -> ret { + let v0 := 0 + switch e + case 1 { + v0 := 1 + } + default { + v0 := 2 + } + ret := v0 } -{ - function match_u8(e) -> ret { - let v0 := 0 - switch e - case 0 { - v0 := 1 - } - case 255 { - v0 := 2 - } - default { - v0 := 3 - } - ret := v0 - } +function match_u8(e) -> ret { + let v0 := 0 + switch e + case 0 { + v0 := 1 + } + case 255 { + v0 := 2 + } + default { + v0 := 3 + } + ret := v0 } -{ - function match_u16(e) -> ret { - let v0 := 0 - switch e - case 4660 { - v0 := 1 - } - case 43981 { - v0 := 2 - } - default { - v0 := 3 - } - ret := v0 - } +function match_u16(e) -> ret { + let v0 := 0 + switch e + case 4660 { + v0 := 1 + } + case 43981 { + v0 := 2 + } + default { + v0 := 3 + } + ret := v0 } -{ - function match_u32(e) -> ret { - let v0 := 0 - switch e - case 3735928559 { - v0 := 1 - } - case 3405691582 { - v0 := 2 - } - default { - v0 := 3 - } - ret := v0 - } +function match_u32(e) -> ret { + let v0 := 0 + switch e + case 3735928559 { + v0 := 1 + } + case 3405691582 { + v0 := 2 + } + default { + v0 := 3 + } + ret := v0 } -{ - function match_u64(e) -> ret { - let v0 := 0 - switch e - case 0 { - v0 := 1 - } - case 18446744073709551615 { - v0 := 2 - } - default { - v0 := 3 - } - ret := v0 - } +function match_u64(e) -> ret { + let v0 := 0 + switch e + case 0 { + v0 := 1 + } + case 18446744073709551615 { + v0 := 2 + } + default { + v0 := 3 + } + ret := v0 } -{ - function match_u128(e) -> ret { - let v0 := 0 - switch e - case 0 { - v0 := 1 - } - case 340282366920938463463374607431768211455 { - v0 := 2 - } - default { - v0 := 3 - } - ret := v0 - } +function match_u128(e) -> ret { + let v0 := 0 + switch e + case 0 { + v0 := 1 + } + case 340282366920938463463374607431768211455 { + v0 := 2 + } + default { + v0 := 3 + } + ret := v0 } -{ - function match_u256(e) -> ret { - let v0 := 0 - switch e - case 0 { - v0 := 1 - } - case 115792089237316195423570985008687907853269984665640564039457584007913129639935 { - v0 := 2 - } - default { - v0 := 3 - } - ret := v0 - } +function match_u256(e) -> ret { + let v0 := 0 + switch e + case 0 { + v0 := 1 + } + case 115792089237316195423570985008687907853269984665640564039457584007913129639935 { + v0 := 2 + } + default { + v0 := 3 + } + ret := v0 } -{ - function match_i8(e) -> ret { - let v0 := 0 - switch e - case 0 { - v0 := 1 - } - case 99 { - v0 := 2 - } - default { - v0 := 3 - } - ret := v0 - } +function match_i8(e) -> ret { + let v0 := 0 + switch e + case 0 { + v0 := 1 + } + case 99 { + v0 := 2 + } + default { + v0 := 3 + } + ret := v0 } -{ - function match_i16(e) -> ret { - let v0 := 0 - switch e - case 0 { - v0 := 1 - } - case 32000 { - v0 := 2 - } - default { - v0 := 3 - } - ret := v0 - } +function match_i16(e) -> ret { + let v0 := 0 + switch e + case 0 { + v0 := 1 + } + case 32000 { + v0 := 2 + } + default { + v0 := 3 + } + ret := v0 } -{ - function match_i32(e) -> ret { - let v0 := 0 - switch e - case 0 { - v0 := 1 - } - case 2000000000 { - v0 := 2 - } - default { - v0 := 3 - } - ret := v0 - } +function match_i32(e) -> ret { + let v0 := 0 + switch e + case 0 { + v0 := 1 + } + case 2000000000 { + v0 := 2 + } + default { + v0 := 3 + } + ret := v0 } -{ - function match_i64(e) -> ret { - let v0 := 0 - switch e - case 0 { - v0 := 1 - } - case 9223372036854775807 { - v0 := 2 - } - default { - v0 := 3 - } - ret := v0 - } +function match_i64(e) -> ret { + let v0 := 0 + switch e + case 0 { + v0 := 1 + } + case 9223372036854775807 { + v0 := 2 + } + default { + v0 := 3 + } + ret := v0 } -{ - function match_i128(e) -> ret { - let v0 := 0 - switch e - case 0 { - v0 := 1 - } - case 170141183460469231731687303715884105727 { - v0 := 2 - } - default { - v0 := 3 - } - ret := v0 - } +function match_i128(e) -> ret { + let v0 := 0 + switch e + case 0 { + v0 := 1 + } + case 170141183460469231731687303715884105727 { + v0 := 2 + } + default { + v0 := 3 + } + ret := v0 } -{ - function match_i256(e) -> ret { - let v0 := 0 - switch e - case 0 { - v0 := 1 - } - case 115792089237316195423570985008687907853269984665640564039457584007913129639935 { - v0 := 2 - } - default { - v0 := 3 - } - ret := v0 - } +function match_i256(e) -> ret { + let v0 := 0 + switch e + case 0 { + v0 := 1 + } + case 115792089237316195423570985008687907853269984665640564039457584007913129639935 { + v0 := 2 + } + default { + v0 := 3 + } + ret := v0 } diff --git a/crates/codegen/tests/fixtures/math_ops.snap b/crates/codegen/tests/fixtures/math_ops.snap index 6c074d2f5..3b169475c 100644 --- a/crates/codegen/tests/fixtures/math_ops.snap +++ b/crates/codegen/tests/fixtures/math_ops.snap @@ -1,10 +1,9 @@ --- source: crates/codegen/tests/yul.rs +assertion_line: 31 expression: output input_file: tests/fixtures/math_ops.fe --- -{ - function math_ops() -> ret { - ret := mod(div(mul(sub(10, 3), 2), 7), 3) - } +function math_ops() -> ret { + ret := mod(div(mul(sub(10, 3), 2), 7), 3) } diff --git a/crates/codegen/tests/fixtures/method_call.snap b/crates/codegen/tests/fixtures/method_call.snap index 53a551905..272e1c91f 100644 --- a/crates/codegen/tests/fixtures/method_call.snap +++ b/crates/codegen/tests/fixtures/method_call.snap @@ -1,26 +1,18 @@ --- source: crates/codegen/tests/yul.rs -assertion_line: 38 +assertion_line: 31 expression: output input_file: tests/fixtures/method_call.fe --- -{ - function make_value() -> ret { - ret := 42 - } +function make_value() -> ret { + ret := 42 } -{ - function gen_value(self) -> ret { - ret := make_value() - } +function foo_gen_value(self) -> ret { + ret := make_value() } -{ - function return_value(self) -> ret { - ret := gen_value(self) - } +function foo_return_value(self) -> ret { + ret := foo_gen_value(self) } -{ - function call_method(foo) -> ret { - ret := return_value(foo) - } +function call_method(foo) -> ret { + ret := foo_return_value(foo) } diff --git a/crates/codegen/tests/fixtures/momo.snap b/crates/codegen/tests/fixtures/momo.snap index 5c320a1c5..610cc618d 100644 --- a/crates/codegen/tests/fixtures/momo.snap +++ b/crates/codegen/tests/fixtures/momo.snap @@ -1,28 +1,27 @@ --- source: crates/codegen/tests/yul.rs -assertion_line: 38 +assertion_line: 31 expression: output input_file: tests/fixtures/momo.fe --- -{ - function read_x(val) -> ret { - ret := add(get_field__MyStruct_u8__fc5e5f8d6bb839cd(val, 0, 0), get_field__MyStruct_u8__fc5e5f8d6bb839cd(val, 0, 1)) - } +function read_x(val) -> ret { + ret := add(get_field__MyStruct_u8__fc5e5f8d6bb839cd(val, 0, 0), get_field__MyStruct_u8__fc5e5f8d6bb839cd(val, 0, 1)) } -{ - function get_field__MyStruct_u8__fc5e5f8d6bb839cd(addr, space, offset) -> ret { - let v1 := add(addr, offset) - let v0 := 0 - switch space - case 0 { - v0 := mload(v1) - } - case 1 { - v0 := sload(v1) - } - default { - } - let v2 := v0 - ret := from_word(v2) - } +function get_field__MyStruct_u8__fc5e5f8d6bb839cd(addr, space, offset) -> ret { + let v1 := add(addr, offset) + let v0 := 0 + switch space + case 0 { + v0 := mload(v1) + } + case 1 { + v0 := sload(v1) + } + default { + } + let v2 := v0 + ret := from_word__u8__bc9d6eeaea22ffb5(v2) +} +function from_word__u8__bc9d6eeaea22ffb5(word) -> ret { + ret := and(word, 255) } diff --git a/crates/codegen/tests/fixtures/ret.snap b/crates/codegen/tests/fixtures/ret.snap index 0773a4bc4..da809f25e 100644 --- a/crates/codegen/tests/fixtures/ret.snap +++ b/crates/codegen/tests/fixtures/ret.snap @@ -1,29 +1,27 @@ --- source: crates/codegen/tests/yul.rs -assertion_line: 38 +assertion_line: 31 expression: output input_file: tests/fixtures/ret.fe --- -{ - function retfoo(b1, x) -> ret { - let v0 := b1 - if v0 { - ret := 0 +function retfoo(b1, x) -> ret { + let v0 := b1 + if v0 { + ret := 0 + } + if iszero(v0) { + let v1 := lt(x, 5) + if v1 { + ret := 1 } - if iszero(v0) { - let v1 := lt(x, 5) - if v1 { - ret := 1 + if iszero(v1) { + let v2 := sub(x, 1) + let v3 := eq(v2, 42) + if v3 { + ret := 2 } - if iszero(v1) { - let v2 := sub(x, 1) - let v3 := eq(v2, 42) - if v3 { - ret := 2 - } - if iszero(v3) { - ret := add(v2, 1) - } + if iszero(v3) { + ret := add(v2, 1) } } } diff --git a/crates/codegen/tests/fixtures/string_literal.snap b/crates/codegen/tests/fixtures/string_literal.snap index 8c4f05e99..70f2d3521 100644 --- a/crates/codegen/tests/fixtures/string_literal.snap +++ b/crates/codegen/tests/fixtures/string_literal.snap @@ -1,10 +1,9 @@ --- source: crates/codegen/tests/yul.rs +assertion_line: 31 expression: output input_file: tests/fixtures/string_literal.fe --- -{ - function string_literal() -> ret { - ret := 0x68656c6c6f - } +function string_literal() -> ret { + ret := 0x68656c6c6f } diff --git a/crates/codegen/tests/fixtures/struct_init.snap b/crates/codegen/tests/fixtures/struct_init.snap index 8d4c7948a..8b42d37f8 100644 --- a/crates/codegen/tests/fixtures/struct_init.snap +++ b/crates/codegen/tests/fixtures/struct_init.snap @@ -1,52 +1,44 @@ --- source: crates/codegen/tests/yul.rs -assertion_line: 38 +assertion_line: 31 expression: output input_file: tests/fixtures/struct_init.fe --- -{ - function make_dummy() -> ret { - let v0 := alloc(40) - store_field__deduped(v0, 0, 0, 42) - store_field__deduped(v0, 0, 8, 99) - let v1 := v0 - ret := v1 +function make_dummy() -> ret { + let v0 := alloc(40) + pop(store_field__deduped(v0, 0, 0, 42)) + pop(store_field__deduped(v0, 0, 8, 99)) + let v1 := v0 + ret := v1 +} +function alloc(size) -> ret { + let v0 := mload(64) + let v1 := eq(v0, 0) + if v1 { + v0 := 128 + mstore(64, add(v0, size)) + ret := v0 + } + if iszero(v1) { + mstore(64, add(v0, size)) + ret := v0 } } -{ - function alloc(size) -> ret { - let v0 := mload(64) - let v1 := eq(v0, 0) - if v1 { - v0 := 128 - mstore(64, add(v0, size)) - ret := v0 +function store_field__deduped(addr, space, offset, value) -> ret { + let v0 := add(addr, offset) + switch space + case 0 { + mstore(v0, to_word__deduped(value)) + ret := 0 } - if iszero(v1) { - mstore(64, add(v0, size)) - ret := v0 + case 1 { + sstore(v0, to_word__deduped(value)) + ret := 0 } - } -} -{ - function store_field__deduped(addr, space, offset, value) -> ret { - let v0 := add(addr, offset) - switch space - case 0 { - mstore(v0, to_word__deduped(value)) - ret := 0 - } - case 1 { - sstore(v0, to_word__deduped(value)) - ret := 0 - } - default { - } - ret := 0 - } + default { + } + ret := 0 } -{ - function to_word__deduped(self) -> ret { - ret := self - } +function to_word__deduped(self) -> ret { + ret := self } diff --git a/crates/codegen/tests/fixtures/tuple_expr.snap b/crates/codegen/tests/fixtures/tuple_expr.snap index 1c48903cc..81b2b092a 100644 --- a/crates/codegen/tests/fixtures/tuple_expr.snap +++ b/crates/codegen/tests/fixtures/tuple_expr.snap @@ -1,10 +1,9 @@ --- source: crates/codegen/tests/yul.rs +assertion_line: 31 expression: output input_file: tests/fixtures/tuple_expr.fe --- -{ - function tuple_expr() -> ret { - ret := tuple(add(1, 2), iszero(0)) - } +function tuple_expr() -> ret { + ret := tuple(add(1, 2), iszero(0)) } diff --git a/crates/codegen/tests/fixtures/unary_expr.snap b/crates/codegen/tests/fixtures/unary_expr.snap index aa8d87a28..a757c6f44 100644 --- a/crates/codegen/tests/fixtures/unary_expr.snap +++ b/crates/codegen/tests/fixtures/unary_expr.snap @@ -1,10 +1,9 @@ --- source: crates/codegen/tests/yul.rs +assertion_line: 31 expression: output input_file: tests/fixtures/unary_expr.fe --- -{ - function unary_expr() -> ret { - ret := sub(0, add(1, 2)) - } +function unary_expr() -> ret { + ret := sub(0, add(1, 2)) } diff --git a/crates/codegen/tests/fixtures/while.snap b/crates/codegen/tests/fixtures/while.snap index ff5d860ee..23191d92e 100644 --- a/crates/codegen/tests/fixtures/while.snap +++ b/crates/codegen/tests/fixtures/while.snap @@ -1,15 +1,13 @@ --- source: crates/codegen/tests/yul.rs -assertion_line: 38 +assertion_line: 31 expression: output input_file: tests/fixtures/while.fe --- -{ - function do_while() -> ret { - let v0 := 0 - for { } lt(v0, 10) { } { - v0 := add(v0, 1) - } - ret := v0 +function do_while() -> ret { + let v0 := 0 + for { } lt(v0, 10) { } { + v0 := add(v0, 1) } + ret := v0 } diff --git a/crates/codegen/tests/yul.rs b/crates/codegen/tests/yul.rs index 7ea821c4e..cbda4557a 100644 --- a/crates/codegen/tests/yul.rs +++ b/crates/codegen/tests/yul.rs @@ -24,14 +24,7 @@ fn yul_snap(fixture: Fixture<&str>) { let top_mod = db.top_mod(file); let output = match emit_module_yul(&db, top_mod) { - Ok(results) => results - .into_iter() - .map(|res| match res { - Ok(yul) => yul, - Err(err) => format!("ERROR: {err}"), - }) - .collect::>() - .join("\n"), + Ok(yul) => yul, Err(err) => format!("MIR ERROR: {err}"), }; diff --git a/crates/contract-harness/Cargo.toml b/crates/contract-harness/Cargo.toml new file mode 100644 index 000000000..7090c257e --- /dev/null +++ b/crates/contract-harness/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "fe-contract-harness" +version = "0.1.0" +edition.workspace = true + +[dependencies] +driver = { path = "../driver", package = "fe-driver" } +codegen = { path = "../codegen", package = "fe-codegen" } +solc_runner = { path = "../solc-runner", package = "fe-solc-runner" } +common = { path = "../common", package = "fe-common" } +revm = { version = "33.1.0", default-features = false, features = ["std"] } +hex = "0.4" +url.workspace = true +ethers-core = "2" +thiserror = "1" + +[dev-dependencies] +serde_json = "1" diff --git a/crates/contract-harness/src/lib.rs b/crates/contract-harness/src/lib.rs new file mode 100644 index 000000000..6925473b9 --- /dev/null +++ b/crates/contract-harness/src/lib.rs @@ -0,0 +1,441 @@ +//! Test harness utilities for compiling Fe contracts and exercising their runtimes with `revm`. +use codegen::emit_module_yul; +use common::InputDb; +use driver::DriverDataBase; +use ethers_core::abi::{AbiParser, ParseError as AbiParseError, Token}; +use hex::FromHex; +pub use revm::primitives::U256; +use revm::{ + bytecode::Bytecode, + context::{ + Context, TxEnv, + result::{ExecutionResult, HaltReason, Output}, + }, + database::InMemoryDB, + handler::{ExecuteCommitEvm, MainBuilder, MainContext, MainnetContext, MainnetEvm}, + primitives::{Address, Bytes as EvmBytes}, + state::AccountInfo, +}; +use solc_runner::{ContractBytecode, YulcError, compile_single_contract}; +use std::{fmt, path::Path}; +use thiserror::Error; +use url::Url; + +/// Default in-memory file path used when compiling inline Fe sources. +const MEMORY_SOURCE_URL: &str = "file:///contract.fe"; + +/// Error type returned by the harness. +#[derive(Debug, Error)] +pub enum HarnessError { + #[error("fe compiler diagnostics:\n{0}")] + CompilerDiagnostics(String), + #[error("failed to emit Yul: {0}")] + EmitYul(#[from] codegen::EmitModuleError), + #[error("solc error: {0}")] + Solc(String), + #[error("abi encoding failed: {0}")] + Abi(#[from] ethers_core::abi::Error), + #[error("failed to parse function signature: {0}")] + AbiSignature(#[from] AbiParseError), + #[error("execution failed: {0}")] + Execution(String), + #[error("runtime reverted with data {0}")] + Revert(RevertData), + #[error("runtime halted: {reason:?} (gas_used={gas_used})")] + Halted { reason: HaltReason, gas_used: u64 }, + #[error("unexpected output variant from runtime")] + UnexpectedOutput, + #[error("invalid hex string: {0}")] + Hex(#[from] hex::FromHexError), + #[error("io error: {0}")] + Io(#[from] std::io::Error), +} + +impl From for HarnessError { + fn from(value: YulcError) -> Self { + Self::Solc(value.0) + } +} + +/// Captures raw revert data and provides a nicer `Display` implementation. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RevertData(pub Vec); + +impl fmt::Display for RevertData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "0x{}", hex::encode(&self.0)) + } +} + +/// Options that control how the Fe source is compiled. +#[derive(Debug, Clone)] +pub struct CompileOptions { + /// Toggle solc optimizer. + pub optimize: bool, + /// Verify that solc produced runtime bytecode. + pub verify_runtime: bool, +} + +impl Default for CompileOptions { + fn default() -> Self { + Self { + optimize: false, + verify_runtime: true, + } + } +} + +/// Options that control the execution context fed into `revm`. +#[derive(Debug, Clone, Copy)] +pub struct ExecutionOptions { + pub caller: Address, + pub gas_limit: u64, + pub gas_price: u128, + pub value: U256, + /// Optional transaction nonce; when absent the harness uses the caller's + /// current nonce from the in-memory database. + pub nonce: Option, +} + +impl Default for ExecutionOptions { + fn default() -> Self { + Self { + caller: Address::ZERO, + gas_limit: 1_000_000, + gas_price: 0, + value: U256::ZERO, + nonce: None, + } + } +} + +/// Output returned from executing contract runtime bytecode. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CallResult { + pub return_data: Vec, + pub gas_used: u64, +} + +fn prepare_account( + runtime_bytecode_hex: &str, +) -> Result<(Bytecode, Address, InMemoryDB), HarnessError> { + let code = hex_to_bytes(runtime_bytecode_hex)?; + let bytecode = Bytecode::new_raw(EvmBytes::from(code)); + let address = Address::with_last_byte(0xff); + Ok((bytecode, address, InMemoryDB::default())) +} + +fn transact( + evm: &mut MainnetEvm>, + address: Address, + calldata: &[u8], + options: ExecutionOptions, + nonce: u64, +) -> Result { + let tx = TxEnv::builder() + .caller(options.caller) + .gas_limit(options.gas_limit) + .gas_price(options.gas_price) + .to(address) + .value(options.value) + .data(EvmBytes::copy_from_slice(calldata)) + .nonce(options.nonce.unwrap_or(nonce)) + .build() + .map_err(|err| HarnessError::Execution(format!("{err:?}")))?; + + let result = evm + .transact_commit(tx) + .map_err(|err| HarnessError::Execution(err.to_string()))?; + match result { + ExecutionResult::Success { + output: Output::Call(bytes), + gas_used, + .. + } => Ok(CallResult { + return_data: bytes.to_vec(), + gas_used, + }), + ExecutionResult::Success { + output: Output::Create(..), + .. + } => Err(HarnessError::UnexpectedOutput), + ExecutionResult::Revert { output, .. } => { + Err(HarnessError::Revert(RevertData(output.to_vec()))) + } + ExecutionResult::Halt { reason, gas_used } => { + Err(HarnessError::Halted { reason, gas_used }) + } + } +} + +/// Stateful runtime instance backed by a persistent in-memory database. +pub struct RuntimeInstance { + evm: MainnetEvm>, + address: Address, + next_nonce: u64, +} + +impl RuntimeInstance { + /// Instantiates a runtime instance from raw bytecode, inserting it into an `InMemoryDB`. + pub fn new(runtime_bytecode_hex: &str) -> Result { + let (bytecode, address, mut db) = prepare_account(runtime_bytecode_hex)?; + let code_hash = bytecode.hash_slow(); + db.insert_account_info( + address, + AccountInfo::new(U256::ZERO, 0, code_hash, bytecode), + ); + let ctx = Context::mainnet().with_db(db); + let evm = ctx.build_mainnet(); + Ok(Self { + evm, + address, + next_nonce: 0, + }) + } + + /// Executes the runtime with arbitrary calldata. + pub fn call_raw( + &mut self, + calldata: &[u8], + options: ExecutionOptions, + ) -> Result { + let nonce = options.nonce.unwrap_or_else(|| { + let current = self.next_nonce; + self.next_nonce += 1; + current + }); + transact(&mut self.evm, self.address, calldata, options, nonce) + } + + /// Executes a strongly-typed function call using ABI encoding. + pub fn call_function( + &mut self, + signature: &str, + args: &[Token], + options: ExecutionOptions, + ) -> Result { + let calldata = encode_function_call(signature, args)?; + self.call_raw(&calldata, options) + } + + /// Returns the contract address assigned to this runtime instance. + pub fn address(&self) -> Address { + self.address + } +} + +/// Harness that compiles Fe source code and executes the resulting contract runtime. +pub struct FeContractHarness { + contract: ContractBytecode, +} + +impl FeContractHarness { + /// Convenience helper that uses default [`CompileOptions`]. + pub fn compile(contract_name: &str, source: &str) -> Result { + Self::compile_from_source(contract_name, source, CompileOptions::default()) + } + + /// Compiles the provided Fe source into bytecode for the specified contract. + pub fn compile_from_source( + contract_name: &str, + source: &str, + options: CompileOptions, + ) -> Result { + let mut db = DriverDataBase::default(); + let url = Url::parse(MEMORY_SOURCE_URL).expect("static URL is valid"); + db.workspace() + .touch(&mut db, url.clone(), Some(source.to_string())); + let file = db + .workspace() + .get(&db, &url) + .expect("file should exist in workspace"); + let top_mod = db.top_mod(file); + let diags = db.run_on_top_mod(top_mod); + if !diags.is_empty() { + return Err(HarnessError::CompilerDiagnostics(diags.format_diags(&db))); + } + let yul = emit_module_yul(&db, top_mod)?; + let contract = compile_single_contract( + contract_name, + &yul, + options.optimize, + options.verify_runtime, + )?; + Ok(Self { contract }) + } + + /// Reads a source file from disk and compiles the specified contract. + pub fn compile_from_file( + contract_name: &str, + path: impl AsRef, + options: CompileOptions, + ) -> Result { + let source = std::fs::read_to_string(path)?; + Self::compile_from_source(contract_name, &source, options) + } + + /// Returns the raw runtime bytecode emitted by `solc`. + pub fn runtime_bytecode(&self) -> &str { + &self.contract.runtime_bytecode + } + + /// Executes the compiled runtime with arbitrary calldata. + pub fn call_raw( + &self, + calldata: &[u8], + options: ExecutionOptions, + ) -> Result { + execute_runtime(&self.contract.runtime_bytecode, calldata, options) + } + + /// ABI-encodes the provided arguments and executes the runtime. + pub fn call_function( + &self, + signature: &str, + args: &[Token], + options: ExecutionOptions, + ) -> Result { + let calldata = encode_function_call(signature, args)?; + self.call_raw(&calldata, options) + } + + /// Creates a persistent runtime instance that can serve multiple calls. + pub fn deploy_instance(&self) -> Result { + RuntimeInstance::new(&self.contract.runtime_bytecode) + } +} + +/// ABI-encodes a function call according to the provided signature. +pub fn encode_function_call(signature: &str, args: &[Token]) -> Result, HarnessError> { + let function = AbiParser::default().parse_function(signature)?; + let encoded = function.encode_input(args)?; + Ok(encoded) +} + +/// Executes the provided runtime bytecode within `revm`. +pub fn execute_runtime( + runtime_bytecode_hex: &str, + calldata: &[u8], + options: ExecutionOptions, +) -> Result { + let mut instance = RuntimeInstance::new(runtime_bytecode_hex)?; + instance.call_raw(calldata, options) +} + +/// Parses a hex string (with or without `0x` prefix) into raw bytes. +pub fn hex_to_bytes(hex: &str) -> Result, HarnessError> { + let trimmed = hex.trim().strip_prefix("0x").unwrap_or(hex.trim()); + Vec::from_hex(trimmed).map_err(HarnessError::Hex) +} + +/// Interprets exactly 32 return bytes as a big-endian `U256`. +pub fn bytes_to_u256(bytes: &[u8]) -> Result { + if bytes.len() != 32 { + return Err(HarnessError::Execution(format!( + "expected 32 bytes of return data, found {}", + bytes.len() + ))); + } + let mut buf = [0u8; 32]; + buf.copy_from_slice(bytes); + Ok(U256::from_be_bytes(buf)) +} + +#[cfg(test)] +mod tests { + use super::*; + use ethers_core::{abi::Token, types::U256 as AbiU256}; + use std::process::Command; + + fn solc_available() -> bool { + let solc_path = std::env::var("FE_SOLC_PATH").unwrap_or_else(|_| "solc".to_string()); + Command::new(solc_path) + .arg("--version") + .status() + .map(|status| status.success()) + .unwrap_or(false) + } + + #[test] + fn runtime_instance_persists_state() { + if !solc_available() { + eprintln!("skipping runtime_instance_persists_state because solc is missing"); + return; + } + let yul = r#" +object "Counter" { + code { + datacopy(0, dataoffset("runtime"), datasize("runtime")) + return(0, datasize("runtime")) + } + object "runtime" { + code { + let current := sload(0) + let next := add(current, 1) + sstore(0, next) + mstore(0x00, next) + return(0x00, 0x20) + } + } +} +"#; + let contract = + compile_single_contract("Counter", yul, false, true).expect("yul compilation succeeds"); + let mut instance = + RuntimeInstance::new(&contract.runtime_bytecode).expect("runtime instantiation"); + let options = ExecutionOptions::default(); + let first = instance + .call_raw(&[0u8; 0], options) + .expect("first call succeeds"); + assert_eq!(bytes_to_u256(&first.return_data).unwrap(), U256::from(1)); + let second = instance + .call_raw(&[0u8; 0], options) + .expect("second call succeeds"); + assert_eq!(bytes_to_u256(&second.return_data).unwrap(), U256::from(2)); + } + + #[test] + fn full_contract_test() { + if !solc_available() { + eprintln!("skipping full_contract_test because solc is missing"); + return; + } + let source_path = concat!( + env!("CARGO_MANIFEST_DIR"), + "/../codegen/tests/fixtures/full_contract.fe" + ); + let harness = FeContractHarness::compile_from_file( + "ShapeDispatcher", + source_path, + CompileOptions::default(), + ) + .expect("compilation should succeed"); + let mut instance = harness.deploy_instance().expect("deployment succeeds"); + let options = ExecutionOptions::default(); + let point_call = encode_function_call( + "point(uint256,uint256)", + &[ + Token::Uint(AbiU256::from(3u64)), + Token::Uint(AbiU256::from(4u64)), + ], + ) + .unwrap(); + let point_result = instance + .call_raw(&point_call, options) + .expect("point selector should succeed"); + assert_eq!( + bytes_to_u256(&point_result.return_data).unwrap(), + U256::from(12u64) + ); + + let square_call = + encode_function_call("square(uint256)", &[Token::Uint(AbiU256::from(5u64))]).unwrap(); + let square_result = instance + .call_raw(&square_call, options) + .expect("square selector should succeed"); + assert_eq!( + bytes_to_u256(&square_result.return_data).unwrap(), + U256::from(25u64) + ); + } +} diff --git a/crates/fe/src/check.rs b/crates/fe/src/check.rs index 37cac5d49..6cd04d5be 100644 --- a/crates/fe/src/check.rs +++ b/crates/fe/src/check.rs @@ -214,19 +214,11 @@ fn print_dependency_info(db: &DriverDataBase, dependency_url: &Url) { fn emit_yul(db: &DriverDataBase, top_mod: TopLevelMod<'_>) { match emit_module_yul(db, top_mod) { - Ok(results) => { - for result in results { - match result { - Ok(yul) => { - println!("=== Yul ==="); - println!("{yul}"); - println!(); - } - Err(err) => eprintln!("⚠️ yul emission skipped: {err}"), - } - } + Ok(yul) => { + println!("=== Yul ==="); + println!("{yul}"); } - Err(err) => eprintln!("⚠️ failed to lower MIR for yul emission: {err}"), + Err(err) => eprintln!("⚠️ failed to emit Yul: {err}"), } } @@ -332,6 +324,11 @@ fn format_terminator(term: &Terminator) -> String { match term { Terminator::Return(Some(val)) => format!("return {}", value_label(*val)), Terminator::Return(None) => "return".into(), + Terminator::ReturnData { offset, size } => format!( + "return_data {}, {}", + value_label(*offset), + value_label(*size) + ), Terminator::Goto { target } => format!("goto bb{}", target.index()), Terminator::Branch { cond, diff --git a/crates/hir/src/analysis/ty/ty_check/env.rs b/crates/hir/src/analysis/ty/ty_check/env.rs index 8a55f8309..5a4ecb934 100644 --- a/crates/hir/src/analysis/ty/ty_check/env.rs +++ b/crates/hir/src/analysis/ty/ty_check/env.rs @@ -194,6 +194,10 @@ impl<'db> TyCheckEnv<'db> { panic!("callable is already registered for the given expr") } } + + pub(super) fn callable_expr(&self, expr: ExprId) -> Option<&Callable<'db>> { + self.callables.get(&expr) + } pub(super) fn binding_name(&self, binding: LocalBinding<'db>) -> IdentId<'db> { binding.binding_name(self) } diff --git a/crates/hir/src/analysis/ty/ty_check/expr.rs b/crates/hir/src/analysis/ty/ty_check/expr.rs index d08dd50b4..86757b9c2 100644 --- a/crates/hir/src/analysis/ty/ty_check/expr.rs +++ b/crates/hir/src/analysis/ty/ty_check/expr.rs @@ -234,19 +234,27 @@ impl<'db> TyChecker<'db> { let Expr::Call(callee, args) = expr_data else { unreachable!() }; - let callee_ty = self.check_expr_unknown(*callee).ty; - if callee_ty.has_invalid(self.db) { + let callee_prop = self.check_expr_unknown(*callee); + if callee_prop.ty.has_invalid(self.db) { return ExprProp::invalid(self.db); } - let mut callable = - match Callable::new(self.db, callee_ty, callee.span(self.body()).into(), None) { + let mut callable = if let Some(existing) = self.env.callable_expr(*callee) { + existing.clone() + } else { + match Callable::new( + self.db, + callee_prop.ty, + callee.span(self.body()).into(), + None, + ) { Ok(callable) => callable, Err(diag) => { self.push_diag(diag); return ExprProp::invalid(self.db); } - }; + } + }; let call_span = expr.span(self.body()).into_call_expr(); @@ -698,7 +706,7 @@ impl<'db> TyChecker<'db> { PathRes::Const(_, ty) => ExprProp::new(ty, true), PathRes::Method(receiver_ty, candidate) => { let canonical_r_ty = Canonicalized::new(self.db, receiver_ty); - let method_ty = match candidate { + let (method_ty, trait_inst) = match candidate { MethodCandidate::InherentMethod(func_def) => { // TODO: move this to path resolver let mut method_ty = TyId::func(self.db, func_def); @@ -713,7 +721,7 @@ impl<'db> TyChecker<'db> { break; } } - method_ty + (method_ty, None) } MethodCandidate::TraitMethod(cand) | MethodCandidate::NeedsConfirmation(cand) => { @@ -722,16 +730,30 @@ impl<'db> TyChecker<'db> { self.env .register_confirmation(inst, path_expr_span.clone().into()); } - super::instantiate_trait_method( + let method_ty = super::instantiate_trait_method( self.db, cand.method, &mut self.table, receiver_ty, inst, - ) + ); + (method_ty, Some(inst)) } }; - ExprProp::new(self.table.instantiate_to_term(method_ty), true) + + let instantiated_method_ty = self.table.instantiate_to_term(method_ty); + if self.env.callable_expr(expr).is_none() { + let callable = Callable::new( + self.db, + instantiated_method_ty, + expr.span(self.body()).into(), + trait_inst, + ) + .expect("method path should resolve to callable"); + self.env.register_callable(expr, callable); + } + + ExprProp::new(instantiated_method_ty, true) } PathRes::TraitConst(_recv_ty, inst, name) => { // Look up the associated const's declared type in the trait and diff --git a/crates/mir/src/hash.rs b/crates/mir/src/hash.rs index 400ae3bbe..fa6166d5f 100644 --- a/crates/mir/src/hash.rs +++ b/crates/mir/src/hash.rs @@ -231,6 +231,13 @@ impl<'db, 'a> FunctionHasher<'db, 'a> { self.write_u8(0); } } + Terminator::ReturnData { offset, size } => { + self.write_u8(0x35); + let offset_slot = self.placeholder_value(*offset); + let size_slot = self.placeholder_value(*size); + self.write_u32(offset_slot); + self.write_u32(size_slot); + } Terminator::Goto { target } => { self.write_u8(0x31); self.write_usize(target.index()); diff --git a/crates/mir/src/ir.rs b/crates/mir/src/ir.rs index 4a5085a26..acd24e2a5 100644 --- a/crates/mir/src/ir.rs +++ b/crates/mir/src/ir.rs @@ -12,7 +12,10 @@ use rustc_hash::FxHashMap; #[derive(Debug, Clone)] pub struct MirModule<'db> { pub top_mod: TopLevelMod<'db>, + /// All lowered functions in the module. pub functions: Vec>, + /// Contracts with their reachable functions already computed. + pub contracts: Vec, } impl<'db> MirModule<'db> { @@ -20,10 +23,20 @@ impl<'db> MirModule<'db> { Self { top_mod, functions: Vec::new(), + contracts: Vec::new(), } } } +/// A contract with its entry point and reachable functions. +#[derive(Debug, Clone)] +pub struct MirContract { + /// The contract's name. + pub name: String, + /// Indices into `MirModule::functions` for all functions reachable from dispatch. + pub function_indices: Vec, +} + /// MIR for a single function. #[derive(Debug, Clone)] pub struct MirFunction<'db> { @@ -189,6 +202,8 @@ pub enum MirInst<'db> { pub enum Terminator { /// Return from the function with an optional value. Return(Option), + /// Return from the function using raw memory pointer/size (core::return_data). + ReturnData { offset: ValueId, size: ValueId }, /// Unconditional jump to another block. Goto { target: BasicBlockId }, /// Conditional branch based on a boolean value. @@ -347,6 +362,8 @@ pub struct IntrinsicValue { pub enum IntrinsicOp { /// `mload(address)` Mload, + /// `calldataload(offset)` + Calldataload, /// `mstore(address, value)` Mstore, /// `mstore8(address, byte)` @@ -355,11 +372,16 @@ pub enum IntrinsicOp { Sload, /// `sstore(slot, value)` Sstore, + /// `return(offset, size)` + ReturnData, } impl IntrinsicOp { /// Returns `true` if this intrinsic yields a value (load), `false` for pure side effects. pub fn returns_value(self) -> bool { - matches!(self, IntrinsicOp::Mload | IntrinsicOp::Sload) + matches!( + self, + IntrinsicOp::Mload | IntrinsicOp::Sload | IntrinsicOp::Calldataload + ) } } diff --git a/crates/mir/src/lib.rs b/crates/mir/src/lib.rs index f131fd394..9d4893762 100644 --- a/crates/mir/src/lib.rs +++ b/crates/mir/src/lib.rs @@ -6,7 +6,7 @@ mod monomorphize; pub use ir::{ BasicBlockId, CallOrigin, LoopInfo, MatchArmLowering, MatchArmPattern, MatchLoweringInfo, - MirBody, MirFunction, MirInst, MirModule, SwitchOrigin, SwitchTarget, SwitchValue, Terminator, - ValueData, ValueId, ValueOrigin, + MirBody, MirContract, MirFunction, MirInst, MirModule, SwitchOrigin, SwitchTarget, SwitchValue, + Terminator, ValueData, ValueId, ValueOrigin, }; pub use lower::{MirLowerError, MirLowerResult, lower_module}; diff --git a/crates/mir/src/lower.rs b/crates/mir/src/lower.rs index 16db16eda..646196ffe 100644 --- a/crates/mir/src/lower.rs +++ b/crates/mir/src/lower.rs @@ -11,16 +11,17 @@ use hir::analysis::{ }, }; use hir::hir_def::{ - Body, CallableDef, Const, Expr, ExprId, Field, FieldIndex, Func, IdentId, LitKind, MatchArm, - Partial, Pat, PatId, PathId, Stmt, StmtId, TopLevelMod, scope_graph::ScopeId, + Body, CallableDef, Const, Contract, Expr, ExprId, Field, FieldIndex, Func, IdentId, ImplTrait, + LitKind, MatchArm, Partial, Pat, PatId, PathId, Stmt, StmtId, TopLevelMod, + scope_graph::ScopeId, }; use crate::{ ir::{ BasicBlock, BasicBlockId, CallOrigin, IntrinsicOp, IntrinsicValue, LoopInfo, - MatchArmLowering, MatchArmPattern, MatchLoweringInfo, MirBody, MirFunction, MirInst, - MirModule, SwitchOrigin, SwitchTarget, SwitchValue, SyntheticValue, Terminator, ValueData, - ValueId, ValueOrigin, + MatchArmLowering, MatchArmPattern, MatchLoweringInfo, MirBody, MirContract, MirFunction, + MirInst, MirModule, SwitchOrigin, SwitchTarget, SwitchValue, SyntheticValue, Terminator, + ValueData, ValueId, ValueOrigin, }, monomorphize::monomorphize_functions, }; @@ -110,7 +111,12 @@ pub fn lower_module<'db>( } let functions = monomorphize_functions(db, templates); - Ok(MirModule { top_mod, functions }) + let contracts = compute_contracts(db, top_mod, &functions); + Ok(MirModule { + top_mod, + functions, + contracts, + }) } /// Lowers a single HIR function (plus its typed body) into MIR form. @@ -131,6 +137,7 @@ pub(crate) fn lower_function<'db>( let mut builder = MirBuilder::new(db, body, &typed_body); let entry = builder.alloc_block(); let fallthrough = builder.lower_root(entry, body.expr(db)); + builder.ensure_const_expr_values(); builder.ensure_field_expr_values(); let ret_val = builder.ensure_value(body.expr(db)); if let Some(block) = fallthrough { @@ -512,6 +519,22 @@ impl<'db, 'a> MirBuilder<'db, 'a> { } } + /// Force constant path expressions to lower into synthetic literals so later stages can + /// emit the literal value instead of the identifier. + fn ensure_const_expr_values(&mut self) { + let exprs = self.body.exprs(self.db); + for expr_id in exprs.keys() { + let Partial::Present(expr) = &exprs[expr_id] else { + continue; + }; + if matches!(expr, Expr::Path(..)) + && let Some(value_id) = self.try_const_expr(expr_id) + { + self.mir_body.expr_values.entry(expr_id).or_insert(value_id); + } + } + } + /// Lower an expression inside a concrete block, returning the exit block and value. fn lower_expr_in( &mut self, @@ -839,6 +862,16 @@ impl<'db, 'a> MirBuilder<'db, 'a> { ) -> Option<(Option, ValueId)> { let (op, args) = self.intrinsic_stmt_args(expr)?; let value_id = self.ensure_value(expr); + if op == IntrinsicOp::ReturnData { + debug_assert!( + args.len() == 2, + "return_data should have exactly two arguments" + ); + let offset = args[0]; + let size = args[1]; + self.set_terminator(block, Terminator::ReturnData { offset, size }); + return Some((None, value_id)); + } self.push_inst(block, MirInst::IntrinsicStmt { expr, op, args }); Some((Some(block), value_id)) } @@ -1124,10 +1157,12 @@ impl<'db, 'a> MirBuilder<'db, 'a> { let name = func_def.name(self.db)?; match name.data(self.db).as_str() { "mload" => Some(IntrinsicOp::Mload), + "calldataload" => Some(IntrinsicOp::Calldataload), "mstore" => Some(IntrinsicOp::Mstore), "mstore8" => Some(IntrinsicOp::Mstore8), "sload" => Some(IntrinsicOp::Sload), "sstore" => Some(IntrinsicOp::Sstore), + "return_data" => Some(IntrinsicOp::ReturnData), _ => None, } } @@ -1480,3 +1515,155 @@ impl<'db, 'a> MirBuilder<'db, 'a> { } } } + +// ============================================================================ +// Contract and reachability analysis +// ============================================================================ + +/// Computes contract metadata by finding dispatcher implementations and their reachable functions. +fn compute_contracts<'db>( + db: &'db dyn HirAnalysisDb, + top_mod: TopLevelMod<'db>, + functions: &[MirFunction<'db>], +) -> Vec { + let call_graph = build_call_graph(functions); + let mut contracts = Vec::new(); + + for contract in top_mod.all_contracts(db).iter().copied() { + let Some(name) = contract_name(db, contract) else { + continue; + }; + let Some(dispatch_func) = find_dispatcher_for_contract(db, top_mod, &name) else { + continue; + }; + let Some(dispatch_idx) = find_function_index(functions, dispatch_func) else { + continue; + }; + let dispatch_symbol = &functions[dispatch_idx].symbol_name; + let reachable_symbols = reachable_functions(&call_graph, dispatch_symbol); + + let function_indices: Vec = functions + .iter() + .enumerate() + .filter(|(_, f)| reachable_symbols.contains(&f.symbol_name)) + .map(|(idx, _)| idx) + .collect(); + + if function_indices.is_empty() { + continue; + } + + contracts.push(MirContract { + name, + function_indices, + }); + } + + contracts +} + +/// Returns the contract name as a string, or `None` when it cannot be resolved. +fn contract_name(db: &dyn HirAnalysisDb, contract: Contract<'_>) -> Option { + contract + .name(db) + .to_opt() + .map(|ident| ident.data(db).to_string()) +} + +/// Finds the `dispatch` method implemented for `contract` via the `Dispatcher` trait. +fn find_dispatcher_for_contract<'db>( + db: &'db dyn HirAnalysisDb, + top_mod: TopLevelMod<'db>, + contract_name: &str, +) -> Option> { + top_mod + .all_impl_traits(db) + .iter() + .copied() + .find_map(|impl_trait| { + if !impl_targets_contract(db, impl_trait, contract_name) + || !is_dispatcher_impl(db, impl_trait) + { + return None; + } + impl_trait.methods(db).find(|func| { + func.name(db) + .to_opt() + .is_some_and(|id| id.data(db) == "dispatch") + }) + }) +} + +/// Returns `true` if `impl_trait` targets the given `contract` type. +fn impl_targets_contract( + db: &dyn HirAnalysisDb, + impl_trait: ImplTrait<'_>, + contract_name: &str, +) -> bool { + let base = impl_trait.ty(db).base_ty(db); + let TyData::TyBase(TyBase::Adt(adt)) = base.data(db) else { + return false; + }; + adt.adt_ref(db) + .name(db) + .is_some_and(|ident| ident.data(db) == contract_name) +} + +/// Returns `true` when the trait reference of `impl_trait` resolves to the core `Dispatcher` trait. +fn is_dispatcher_impl(db: &dyn HirAnalysisDb, impl_trait: ImplTrait<'_>) -> bool { + let Some(trait_def) = impl_trait.trait_def(db) else { + return false; + }; + let name_matches = trait_def + .name(db) + .to_opt() + .is_some_and(|ident| ident.data(db) == "Dispatcher"); + let ingot_kind = trait_def.top_mod(db).ingot(db).kind(db); + name_matches && ingot_kind == IngotKind::Core +} + +/// Builds an adjacency list of calls between lowered functions keyed by their symbol name. +fn build_call_graph<'db>(functions: &[MirFunction<'db>]) -> FxHashMap> { + let mut graph = FxHashMap::default(); + let known: FxHashSet<_> = functions + .iter() + .map(|func| func.symbol_name.clone()) + .collect(); + + for func in functions { + let mut callees = FxHashSet::default(); + for value in &func.body.values { + if let ValueOrigin::Call(call) = &value.origin + && let Some(target) = &call.resolved_name + && known.contains(target) + { + callees.insert(target.clone()); + } + } + graph.insert(func.symbol_name.clone(), callees.into_iter().collect()); + } + + graph +} + +/// Walks the call graph from `root` and returns all reachable symbols (including the root). +fn reachable_functions(graph: &FxHashMap>, root: &str) -> FxHashSet { + let mut visited = FxHashSet::default(); + let mut stack = vec![root.to_string()]; + while let Some(symbol) = stack.pop() { + if !visited.insert(symbol.clone()) { + continue; + } + if let Some(children) = graph.get(&symbol) { + for child in children { + stack.push(child.clone()); + } + } + } + visited +} + +/// Finds the index of the MIR function corresponding to `func` in the functions list. +fn find_function_index<'db>(functions: &[MirFunction<'db>], func: Func<'db>) -> Option { + functions.iter().position(|mir_func| mir_func.func == func) +} diff --git a/crates/mir/src/monomorphize.rs b/crates/mir/src/monomorphize.rs index 6a24d95fb..bcd32c4dd 100644 --- a/crates/mir/src/monomorphize.rs +++ b/crates/mir/src/monomorphize.rs @@ -12,7 +12,7 @@ use hir::analysis::{ ty_def::{TyData, TyId}, }, }; -use hir::hir_def::{CallableDef, Func}; +use hir::hir_def::{CallableDef, Func, item::ItemKind, scope_graph::ScopeId}; use rustc_hash::FxHashMap; use crate::{CallOrigin, MirFunction, ValueOrigin, dedup::deduplicate_mir, lower::lower_function}; @@ -195,11 +195,14 @@ impl<'db> Monomorphizer<'db> { /// Produce a globally unique (yet mostly readable) symbol name per instance. fn mangled_name(&self, func: Func<'db>, args: &[TyId<'db>]) -> String { - let base = func + let mut base = func .name(self.db) .to_opt() .map(|ident| ident.data(self.db).to_string()) .unwrap_or_else(|| "".into()); + if let Some(prefix) = self.associated_prefix(func) { + base = format!("{prefix}_{base}"); + } if args.is_empty() { return base; } @@ -216,6 +219,23 @@ impl<'db> Monomorphizer<'db> { format!("{base}__{suffix}__{hash:08x}") } + /// Returns a sanitized prefix for associated functions/methods based on their owner. + fn associated_prefix(&self, func: Func<'db>) -> Option { + let parent = func.scope().parent(self.db)?; + let ScopeId::Item(item) = parent else { + return None; + }; + if let ItemKind::Impl(impl_block) = item { + let ty = impl_block.ty(self.db); + if ty.has_invalid(self.db) { + return None; + } + Some(sanitize_symbol_component(ty.pretty_print(self.db)).to_lowercase()) + } else { + None + } + } + fn into_instances(self) -> Vec> { self.instances } diff --git a/crates/solc-runner/Cargo.toml b/crates/solc-runner/Cargo.toml index be82451b6..438673268 100644 --- a/crates/solc-runner/Cargo.toml +++ b/crates/solc-runner/Cargo.toml @@ -8,5 +8,4 @@ indexmap = "2.6.0" serde_json = "1.0.133" [dev-dependencies] -hex = "0.4.3" -revm = { version = "33.1.0", default-features = false, features = ["std"] } +contract-harness = { path = "../contract-harness", package = "fe-contract-harness" } diff --git a/crates/solc-runner/src/lib.rs b/crates/solc-runner/src/lib.rs index a7bb3e928..cf34b3549 100644 --- a/crates/solc-runner/src/lib.rs +++ b/crates/solc-runner/src/lib.rs @@ -216,17 +216,17 @@ fn extract_object(value: &Value, path: &[&str]) -> Option { #[cfg(test)] mod tests { use super::*; - use revm::{ - bytecode::Bytecode, - context::{ - Context, TxEnv, - result::{ExecutionResult, Output}, - }, - database::InMemoryDB, - handler::{ExecuteCommitEvm, MainBuilder, MainContext}, - primitives::{Address, Bytes as EvmBytes, U256}, - state::AccountInfo, - }; + use contract_harness::{ExecutionOptions, U256, bytes_to_u256, execute_runtime}; + use std::process::Command; + + fn solc_available() -> bool { + let solc_path = std::env::var(super::SOLC_ENV).unwrap_or_else(|_| "solc".to_string()); + Command::new(solc_path) + .arg("--version") + .status() + .map(|status| status.success()) + .unwrap_or(false) + } #[test] fn build_standard_json_contains_fields() { let json_str = build_standard_json("{ sstore(0, 0) }", false).unwrap(); @@ -238,6 +238,10 @@ mod tests { #[test] fn executes_contract_function() { + if !solc_available() { + eprintln!("skipping executes_contract_function because solc is missing"); + return; + } let yul = r#" object "Double" { code { @@ -256,8 +260,16 @@ object "Double" { let contract = compile_single_contract("Double", yul, false, true) .expect("solc should compile handwritten contract"); let calldata = encode_call_data(10u64); - let output = execute_runtime(&contract.runtime_bytecode, &calldata); - assert_eq!(bytes_to_u256(&output), U256::from(20u64)); + let result = execute_runtime( + &contract.runtime_bytecode, + &calldata, + ExecutionOptions::default(), + ) + .expect("runtime execution should succeed"); + assert_eq!( + bytes_to_u256(&result.return_data).expect("return data should encode a u256"), + U256::from(20u64) + ); } /// Builds calldata for the `Double` contract by ABI-encoding a single `u64`. @@ -271,63 +283,5 @@ object "Double" { data } - /// Executes the provided runtime bytecode and returns the raw call output bytes. - /// - /// * `bytecode_hex` - Hex string representation of the runtime bytecode. - /// * `calldata` - ABI-encoded calldata fed into the runtime. - /// - /// Returns the raw output returned by the EVM execution. - fn execute_runtime(bytecode_hex: &str, calldata: &[u8]) -> Vec { - let code = hex_to_bytes(bytecode_hex); - let bytecode = Bytecode::new_raw(EvmBytes::from(code)); - let code_hash = bytecode.hash_slow(); - let mut db = InMemoryDB::default(); - let address = Address::with_last_byte(0xff); - let account = AccountInfo::new(U256::ZERO, 0, code_hash, bytecode); - db.insert_account_info(address, account); - - let ctx = Context::mainnet().with_db(db); - let mut evm = ctx.build_mainnet(); - - let tx = TxEnv::builder() - .caller(Address::ZERO) - .gas_limit(1_000_000) - .gas_price(0) - .to(address) - .value(U256::ZERO) - .data(EvmBytes::copy_from_slice(calldata)) - .build() - .expect("tx builder should succeed"); - - let result = evm - .transact_commit(tx) - .expect("runtime execution should succeed"); - match result { - ExecutionResult::Success { - output: Output::Call(bytes), - .. - } => bytes.to_vec(), - other => panic!("runtime execution failed: {other:?}"), - } - } - - /// Converts a hex-encoded bytecode string (with or without `0x`) into raw bytes. - /// - /// * `hex` - Hexadecimal string to decode. - /// - /// Returns a vector containing the decoded bytes. - fn hex_to_bytes(hex: &str) -> Vec { - let trimmed = hex.trim().strip_prefix("0x").unwrap_or(hex.trim()); - hex::decode(trimmed).expect("runtime bytecode should be valid hex") - } - - /// Interprets a 32-byte slice as a big-endian `U256`. - /// - /// * `bytes` - Slice containing the execution result bytes. - /// - /// Returns the value as `U256`, panicking if the slice is not exactly 32 bytes. - fn bytes_to_u256(bytes: &[u8]) -> U256 { - let array: [u8; 32] = bytes.try_into().expect("expected 32 bytes of return data"); - U256::from_be_bytes(array) - } + // execute_runtime and helpers are provided by the contract-harness crate. } diff --git a/library/core/src/dispatcher.fe b/library/core/src/dispatcher.fe new file mode 100644 index 000000000..2895dfabd --- /dev/null +++ b/library/core/src/dispatcher.fe @@ -0,0 +1,10 @@ +/// Minimal interface that Fe contracts need to implement so the generated +/// runtime can delegate control to user code. +pub trait Dispatcher { + /// Entry point invoked from the generated Yul runtime during every call. + /// + /// The function does not return a value. Implementations should handle + /// decoding calldata, invoking user logic, and returning or reverting as + /// needed using the available intrinsics. + fn dispatch() +} diff --git a/library/core/src/intrinsic.fe b/library/core/src/intrinsic.fe index a3f0b785e..f82c6b902 100644 --- a/library/core/src/intrinsic.fe +++ b/library/core/src/intrinsic.fe @@ -4,8 +4,12 @@ extern { /// Load a 32-byte word from memory. pub fn mload(_: u256) -> u256 + /// Load a 32-byte word from calldata. + pub fn calldataload(_: u256) -> u256 /// Store a 32-byte word to memory. pub fn mstore(_: u256, _: u256) + /// Return `size` bytes starting at `offset` to the caller. + pub fn return_data(_: u256, _: u256) /// Store the low byte of the second argument at the memory address. pub fn mstore8(_: u256, _: u8) /// Load a 32-byte word from storage. diff --git a/library/core/src/lib.fe b/library/core/src/lib.fe index 145cbc096..e4db8eec1 100644 --- a/library/core/src/lib.fe +++ b/library/core/src/lib.fe @@ -1,9 +1,10 @@ pub use option::Option pub use result::Result pub use default::Default +pub use dispatcher::Dispatcher pub use ops::* pub use ptr::* -pub use intrinsic::{mload, mstore, mstore8, sload, sstore} +pub use intrinsic::{calldataload, mload, mstore, mstore8, return_data, sload, sstore} pub use mem::alloc pub use functional::{Fn, Functor, Applicative, Monad}