From 2111b161a95b493528e05a2fcc90c99d6488f6b8 Mon Sep 17 00:00:00 2001 From: brockelmore <31553173+brockelmore@users.noreply.github.com> Date: Tue, 10 Mar 2026 20:32:51 -0700 Subject: [PATCH 1/5] feat: trait method calls on primitives, span-based errors, break/continue warnings Enable dot-syntax for compiler-provided trait methods on primitive types (e.g., `a.unsafe_add(b)` instead of `UnsafeAdd::unsafe_add(a, b)`). Supports chained calls and complex receiver expressions. Convert all `IrError::Lowering(String)` to span-based `IrError::Diagnostic` for better error messages with source locations. Remove the now-unused `Lowering` variant. Emit compiler warnings for unimplemented `break`/`continue`. Co-Authored-By: Claude Opus 4.6 --- crates/ir/src/lib.rs | 3 - crates/ir/src/to_egglog/calls.rs | 151 +++++++++++++++---- crates/ir/src/to_egglog/composite.rs | 61 ++++++-- crates/ir/src/to_egglog/control_flow.rs | 18 ++- crates/ir/src/to_egglog/expr.rs | 57 ++++--- crates/ir/src/to_egglog/pattern.rs | 6 +- crates/ir/src/to_egglog/storage.rs | 9 +- examples/tests/test_full_math.edge | 30 ++-- examples/tests/test_inlined_halt.edge | 6 +- examples/tests/test_method_on_primitive.edge | 26 ++++ examples/tests/test_signed_widths.edge | 6 +- examples/tests/test_unsafe_arith.edge | 12 +- 12 files changed, 289 insertions(+), 96 deletions(-) create mode 100644 examples/tests/test_method_on_primitive.edge diff --git a/crates/ir/src/lib.rs b/crates/ir/src/lib.rs index 040854f..ae7a202 100644 --- a/crates/ir/src/lib.rs +++ b/crates/ir/src/lib.rs @@ -45,9 +45,6 @@ pub use schema::{EvmContract, EvmExpr, EvmProgram, RcExpr}; /// Errors that can occur during IR lowering or optimization. #[derive(Debug, thiserror::Error)] pub enum IrError { - /// Error during AST lowering - #[error("lowering error: {0}")] - Lowering(String), /// Error during AST lowering with source span for diagnostics #[error("{message}")] LoweringSpanned { diff --git a/crates/ir/src/to_egglog/calls.rs b/crates/ir/src/to_egglog/calls.rs index cfe579d..d232137 100644 --- a/crates/ir/src/to_egglog/calls.rs +++ b/crates/ir/src/to_egglog/calls.rs @@ -23,20 +23,20 @@ impl AstToEgglog { let type_name = &components[0].name; let variant_name = &components[1].name; if self.union_types.contains_key(type_name) { - return self.lower_union_instantiation_expr(type_name, variant_name, args); + return self.lower_union_instantiation_expr(type_name, variant_name, args, Some(span)); } // Check for generic union types (e.g., Result::Ok(42) where Result was monomorphized) if self.generic_type_templates.contains_key(type_name) { // First try to find an already-monomorphized version if let Some(mangled) = self.resolve_generic_type_name(type_name) { - return self.lower_union_instantiation_expr(&mangled, variant_name, args); + return self.lower_union_instantiation_expr(&mangled, variant_name, args, Some(span)); } // No monomorphized version yet — try to infer type params from // the constructor argument and monomorphize on the fly. if let Some(mangled) = self.try_monomorphize_union_from_constructor(type_name, variant_name, args)? { - return self.lower_union_instantiation_expr(&mangled, variant_name, args); + return self.lower_union_instantiation_expr(&mangled, variant_name, args, Some(span)); } return Err(IrError::Diagnostic( edge_diagnostics::Diagnostic::error(format!( @@ -199,6 +199,23 @@ impl AstToEgglog { .collect(); return self.inline_function_call(¶ms, &body, &all_args); } + + // Check compiler-provided trait methods for primitive types + if Self::is_primitive_type(type_name) { + if let Some(op) = self.compiler_provided_method(method_name) { + if args.len() != 1 { + return Err(IrError::Diagnostic( + edge_diagnostics::Diagnostic::error(format!( + "`.{method_name}()` expects exactly 1 argument", + )) + .with_label(span.clone(), "expected 1 argument"), + )); + } + let lhs = self.lower_expr(receiver)?; + let rhs = self.lower_expr(&args[0])?; + return Ok(ast_helpers::bop(op, lhs, rhs)); + } + } } // If receiver type is known but no method found, give a clear error @@ -223,6 +240,25 @@ impl AstToEgglog { return Err(IrError::Diagnostic(diag)); } + // When receiver type is unknown, try compiler-provided trait methods + // (handles chained calls like `a.unsafe_add(b).unsafe_sub(c)`, + // paren expressions, and other cases where type inference fails) + if receiver_type.is_none() { + if let Some(op) = self.compiler_provided_method(method_name) { + if args.len() != 1 { + return Err(IrError::Diagnostic( + edge_diagnostics::Diagnostic::error(format!( + "`.{method_name}()` expects exactly 1 argument", + )) + .with_label(span.clone(), "expected 1 argument"), + )); + } + let lhs = self.lower_expr(receiver)?; + let rhs = self.lower_expr(&args[0])?; + return Ok(ast_helpers::bop(op, lhs, rhs)); + } + } + // Fallback: treat as FunctionCall(FieldAccess(...), args) — lower normally let _field_access = self.lower_field_access(receiver, method_name)?; let args_ir: Vec = args @@ -282,31 +318,27 @@ impl AstToEgglog { )); } - // Built-in UnsafeAdd/UnsafeSub/UnsafeMul for primitives - let unsafe_op = match (trait_name, method_name) { - ("UnsafeAdd", "unsafe_add") => Some(EvmBinaryOp::Add), - ("UnsafeSub", "unsafe_sub") => Some(EvmBinaryOp::Sub), - ("UnsafeMul", "unsafe_mul") => Some(EvmBinaryOp::Mul), - _ => None, - }; - if let Some(op) = unsafe_op { - // Check if receiver is a primitive (not a user-defined type) + // Compiler-provided trait methods for primitive types + { let receiver_type = self.infer_receiver_type(&args[0]); - if receiver_type.is_none() { - // Primitive type — emit unchecked op directly - if args.len() != 2 { - return Err(IrError::Diagnostic( - edge_diagnostics::Diagnostic::error(format!( - "`{trait_name}::{method_name}` expects exactly 2 arguments", - )) - .with_label(span.clone(), "expected 2 arguments"), - )); + let is_primitive = receiver_type + .as_ref() + .map_or(true, |t| Self::is_primitive_type(t)); + if is_primitive { + if let Some(op) = self.compiler_provided_method(method_name) { + if args.len() != 2 { + return Err(IrError::Diagnostic( + edge_diagnostics::Diagnostic::error(format!( + "`{trait_name}::{method_name}` expects exactly 2 arguments", + )) + .with_label(span.clone(), "expected 2 arguments"), + )); + } + let lhs = self.lower_expr(&args[0])?; + let rhs = self.lower_expr(&args[1])?; + return Ok(ast_helpers::bop(op, lhs, rhs)); } - let lhs = self.lower_expr(&args[0])?; - let rhs = self.lower_expr(&args[1])?; - return Ok(ast_helpers::bop(op, lhs, rhs)); } - // User-defined type — fall through to trait impl lookup } // Try to infer receiver type @@ -457,17 +489,84 @@ impl AstToEgglog { pub(crate) fn infer_receiver_type(&self, expr: &edge_ast::Expr) -> Option { match expr { edge_ast::Expr::Ident(ident) => { - // Check scope for composite type info for scope in self.scopes.iter().rev() { if let Some(binding) = scope.bindings.get(&ident.name) { + // Composite type (struct/union/array) takes priority if let Some(ref ct) = binding.composite_type { return Some(ct.clone()); } + // Fall back to primitive type name from EvmType + return Self::evm_type_to_name(&binding._ty); } } None } edge_ast::Expr::StructInstantiation(_, type_name, _, _) => Some(type_name.name.clone()), + edge_ast::Expr::Literal(lit) => match lit.as_ref() { + edge_ast::Lit::Bool(_, _) => Some("bool".to_string()), + edge_ast::Lit::Int(_, Some(pt), _) => { + Some(Self::primitive_type_to_name(pt)) + } + edge_ast::Lit::Int(_, None, _) => Some("u256".to_string()), + _ => None, + }, + _ => None, + } + } + + /// Convert an EvmType to a type name string (for primitives). + fn evm_type_to_name(ty: &EvmType) -> Option { + match ty { + EvmType::Base(base) => match base { + EvmBaseType::UIntT(256) => Some("u256".to_string()), + EvmBaseType::UIntT(w) => Some(format!("u{w}")), + EvmBaseType::IntT(256) => Some("i256".to_string()), + EvmBaseType::IntT(w) => Some(format!("i{w}")), + EvmBaseType::BoolT => Some("bool".to_string()), + EvmBaseType::AddrT => Some("address".to_string()), + EvmBaseType::BytesT(n) => Some(format!("bytes{n}")), + EvmBaseType::UnitT | EvmBaseType::StateT => None, + }, + _ => None, + } + } + + /// Convert a PrimitiveType to a type name string. + fn primitive_type_to_name(pt: &edge_ast::ty::PrimitiveType) -> String { + use edge_ast::ty::PrimitiveType; + match pt { + PrimitiveType::UInt(256) => "u256".to_string(), + PrimitiveType::UInt(w) => format!("u{w}"), + PrimitiveType::Int(256) => "i256".to_string(), + PrimitiveType::Int(w) => format!("i{w}"), + PrimitiveType::Bool => "bool".to_string(), + PrimitiveType::Address => "address".to_string(), + PrimitiveType::FixedBytes(n) => format!("bytes{n}"), + PrimitiveType::Bit => "bit".to_string(), + } + } + + /// Check if a type name refers to a primitive type (not a user-defined composite). + pub(crate) fn is_primitive_type(type_name: &str) -> bool { + type_name == "u256" + || type_name == "i256" + || type_name == "bool" + || type_name == "address" + || type_name.starts_with("u") + && type_name[1..].parse::().is_ok() + || type_name.starts_with("i") + && type_name[1..].parse::().is_ok() + || type_name.starts_with("bytes") + && type_name[5..].parse::().is_ok() + } + + /// Look up a compiler-provided trait method for a primitive type. + /// Returns the binary op if the method matches an imported std::ops trait. + fn compiler_provided_method(&self, method_name: &str) -> Option { + match method_name { + "unsafe_add" if self.std_ops_traits.contains("UnsafeAdd") => Some(EvmBinaryOp::Add), + "unsafe_sub" if self.std_ops_traits.contains("UnsafeSub") => Some(EvmBinaryOp::Sub), + "unsafe_mul" if self.std_ops_traits.contains("UnsafeMul") => Some(EvmBinaryOp::Mul), _ => None, } } diff --git a/crates/ir/src/to_egglog/composite.rs b/crates/ir/src/to_egglog/composite.rs index 8c98791..d690043 100644 --- a/crates/ir/src/to_egglog/composite.rs +++ b/crates/ir/src/to_egglog/composite.rs @@ -8,6 +8,7 @@ use crate::{ schema::{EvmBaseType, EvmBinaryOp, EvmType, RcExpr}, IrError, }; +use edge_diagnostics; impl AstToEgglog { /// Look up the variant index for a union type. @@ -16,28 +17,44 @@ impl AstToEgglog { &self, type_name: &str, variant_name: &str, + span: Option<&edge_types::span::Span>, ) -> Result { // Try direct lookup first let variants = if let Some(v) = self.union_types.get(type_name) { v } else if let Some(mangled) = self.resolve_generic_type_name(type_name) { - self.union_types - .get(&mangled) - .ok_or_else(|| IrError::Lowering(format!("unknown union type: {type_name}")))? + self.union_types.get(&mangled).ok_or_else(|| { + let diag = + edge_diagnostics::Diagnostic::error(format!("unknown union type: `{type_name}`")); + IrError::Diagnostic(if let Some(s) = span { + diag.with_label(s.clone(), "not found") + } else { + diag + }) + })? } else { - return Err(IrError::Lowering(format!( - "unknown union type: {type_name}" - ))); + let diag = + edge_diagnostics::Diagnostic::error(format!("unknown union type: `{type_name}`")); + return Err(IrError::Diagnostic(if let Some(s) = span { + diag.with_label(s.clone(), "not found") + } else { + diag + })); }; variants .iter() .position(|(name, _)| name == variant_name) .ok_or_else(|| { let available: Vec<&str> = variants.iter().map(|(n, _)| n.as_str()).collect(); - IrError::Lowering(format!( - "no variant named `{variant_name}` in union `{type_name}`; available variants: {}", - available.join(", "), + let diag = edge_diagnostics::Diagnostic::error(format!( + "no variant named `{variant_name}` in union `{type_name}`", )) + .with_note(format!("available variants: {}", available.join(", "))); + IrError::Diagnostic(if let Some(s) = span { + diag.with_label(s.clone(), "variant not found") + } else { + diag + }) }) } @@ -49,19 +66,37 @@ impl AstToEgglog { type_name: &str, variant_name: &str, args: &[edge_ast::Expr], + span: Option<&edge_types::span::Span>, ) -> Result { - let idx = self.variant_index(type_name, variant_name)?; + let idx = self.variant_index(type_name, variant_name, span)?; // Resolve generic type names to monomorphized versions let resolved_name = if self.union_types.contains_key(type_name) { type_name.to_string() } else { - self.resolve_generic_type_name(type_name) - .ok_or_else(|| IrError::Lowering(format!("unknown union type: {type_name}")))? + self.resolve_generic_type_name(type_name).ok_or_else(|| { + let diag = edge_diagnostics::Diagnostic::error(format!( + "unknown union type: `{type_name}`", + )); + IrError::Diagnostic(if let Some(s) = span { + diag.with_label(s.clone(), "not found") + } else { + diag + }) + })? }; let variants = self .union_types .get(&resolved_name) - .ok_or_else(|| IrError::Lowering(format!("unknown union type: {type_name}")))?; + .ok_or_else(|| { + let diag = edge_diagnostics::Diagnostic::error(format!( + "unknown union type: `{type_name}`", + )); + IrError::Diagnostic(if let Some(s) = span { + diag.with_label(s.clone(), "not found") + } else { + diag + }) + })?; let has_data = variants.get(idx).map(|(_, d)| *d).unwrap_or(false); if !has_data || args.is_empty() { diff --git a/crates/ir/src/to_egglog/control_flow.rs b/crates/ir/src/to_egglog/control_flow.rs index e75bd43..50ba542 100644 --- a/crates/ir/src/to_egglog/control_flow.rs +++ b/crates/ir/src/to_egglog/control_flow.rs @@ -293,8 +293,22 @@ impl AstToEgglog { let item_ir = match item { edge_ast::LoopItem::Stmt(stmt) => self.lower_stmt(stmt)?, edge_ast::LoopItem::Expr(expr) => self.lower_expr(expr)?, - edge_ast::LoopItem::Break(_) | edge_ast::LoopItem::Continue(_) => { - // TODO: handle break/continue with control flow markers + edge_ast::LoopItem::Break(span) => { + self.warnings.push( + edge_diagnostics::Diagnostic::warning( + "`break` is not yet implemented and will be ignored", + ) + .with_label(span.clone(), "has no effect"), + ); + continue; + } + edge_ast::LoopItem::Continue(span) => { + self.warnings.push( + edge_diagnostics::Diagnostic::warning( + "`continue` is not yet implemented and will be ignored", + ) + .with_label(span.clone(), "has no effect"), + ); continue; } }; diff --git a/crates/ir/src/to_egglog/expr.rs b/crates/ir/src/to_egglog/expr.rs index 8d5011a..9c308d6 100644 --- a/crates/ir/src/to_egglog/expr.rs +++ b/crates/ir/src/to_egglog/expr.rs @@ -283,9 +283,25 @@ impl AstToEgglog { edge_ast::Stmt::Expr(expr) => self.lower_expr(expr), - edge_ast::Stmt::Break(_) | edge_ast::Stmt::Continue(_) => { - // Break/continue need special handling within loop context - // For now, return empty + edge_ast::Stmt::Break(span) => { + self.warnings.push( + edge_diagnostics::Diagnostic::warning( + "`break` is not yet implemented and will be ignored", + ) + .with_label(span.clone(), "has no effect"), + ); + Ok(ast_helpers::empty( + EvmType::Base(EvmBaseType::UnitT), + self.current_ctx.clone(), + )) + } + edge_ast::Stmt::Continue(span) => { + self.warnings.push( + edge_diagnostics::Diagnostic::warning( + "`continue` is not yet implemented and will be ignored", + ) + .with_label(span.clone(), "has no effect"), + ); Ok(ast_helpers::empty( EvmType::Base(EvmBaseType::UnitT), self.current_ctx.clone(), @@ -494,13 +510,13 @@ impl AstToEgglog { self.lower_field_access(obj, &field.name) } - edge_ast::Expr::Path(components, _span) => { + edge_ast::Expr::Path(components, span) => { // Check if this is a union variant path like Direction::North if components.len() == 2 { let type_name = &components[0].name; let variant_name = &components[1].name; if self.union_types.contains_key(type_name) { - return self.lower_union_instantiation_expr(type_name, variant_name, &[]); + return self.lower_union_instantiation_expr(type_name, variant_name, &[], Some(span)); } // Check for generic union types (e.g., Option::None where Option was monomorphized) if self.generic_type_templates.contains_key(type_name) { @@ -509,6 +525,7 @@ impl AstToEgglog { &mangled, variant_name, &[], + Some(span), ); } } @@ -571,8 +588,8 @@ impl AstToEgglog { self.lower_array_instantiation(elements) } - edge_ast::Expr::UnionInstantiation(type_name, variant_name, args, _span) => { - self.lower_union_instantiation_expr(&type_name.name, &variant_name.name, args) + edge_ast::Expr::UnionInstantiation(type_name, variant_name, args, span) => { + self.lower_union_instantiation_expr(&type_name.name, &variant_name.name, args, Some(span)) } edge_ast::Expr::PatternMatch(expr, pattern, _span) => { @@ -691,17 +708,16 @@ impl AstToEgglog { }; } } - span.map_or_else( - || Err(IrError::Lowering(format!("undefined variable: {name}"))), - |span| { - Err(IrError::Diagnostic( - edge_diagnostics::Diagnostic::error(format!( - "cannot find value `{name}` in this scope", - )) - .with_label(span.clone(), "not found in this scope"), - )) - }, - ) + // Always emit a Diagnostic error — use span when available + let diag = edge_diagnostics::Diagnostic::error(format!( + "cannot find value `{name}` in this scope", + )); + let diag = if let Some(span) = span { + diag.with_label(span.clone(), "not found in this scope") + } else { + diag + }; + Err(IrError::Diagnostic(diag)) } /// Lower an assignment expression. @@ -1163,9 +1179,12 @@ impl AstToEgglog { None => return Ok(None), }; - // Check if the LHS is a user-defined type + // Check if the LHS is a user-defined type (skip primitives — they use built-in ops) let lhs_type = self.infer_receiver_type(lhs); if let Some(ref type_name) = lhs_type { + if Self::is_primitive_type(type_name) { + return Ok(None); + } // Only dispatch to operator traits from std::ops. // User-defined traits named "Add" etc. do NOT get operator overloading. if !self.std_ops_traits.contains(trait_name) { diff --git a/crates/ir/src/to_egglog/pattern.rs b/crates/ir/src/to_egglog/pattern.rs index 836e916..9b5b4fb 100644 --- a/crates/ir/src/to_egglog/pattern.rs +++ b/crates/ir/src/to_egglog/pattern.rs @@ -17,7 +17,7 @@ impl AstToEgglog { pattern: &edge_ast::pattern::UnionPattern, ) -> Result { let disc_ir = self.lower_expr(expr)?; - let idx = self.variant_index(&pattern.union_name.name, &pattern.member_name.name)?; + let idx = self.variant_index(&pattern.union_name.name, &pattern.member_name.name, Some(&pattern.span))?; let idx_ir = ast_helpers::const_int(idx as i64, self.current_ctx.clone()); Ok(ast_helpers::eq(disc_ir, idx_ir)) } @@ -73,7 +73,7 @@ impl AstToEgglog { for arm in arms { match &arm.pattern { edge_ast::pattern::MatchPattern::Union(up) => { - let idx = self.variant_index(&up.union_name.name, &up.member_name.name)?; + let idx = self.variant_index(&up.union_name.name, &up.member_name.name, Some(&up.span))?; let bindings: Vec = up.bindings.iter().map(|b| b.name.clone()).collect(); variant_arms.push((idx, &arm.body, bindings)); @@ -176,7 +176,7 @@ impl AstToEgglog { Rc::clone(&disc_ir) }; - let idx = self.variant_index(&pattern.union_name.name, &pattern.member_name.name)?; + let idx = self.variant_index(&pattern.union_name.name, &pattern.member_name.name, Some(&pattern.span))?; let idx_ir = ast_helpers::const_int(idx as i64, self.current_ctx.clone()); let cond = ast_helpers::eq(disc_val, idx_ir); let inputs = diff --git a/crates/ir/src/to_egglog/storage.rs b/crates/ir/src/to_egglog/storage.rs index 9ea79e0..9fa9164 100644 --- a/crates/ir/src/to_egglog/storage.rs +++ b/crates/ir/src/to_egglog/storage.rs @@ -8,6 +8,7 @@ use crate::{ schema::{DataLocation, EvmExpr, RcExpr}, IrError, }; +use edge_diagnostics; impl AstToEgglog { /// Lower an emit statement. @@ -333,8 +334,10 @@ impl AstToEgglog { } } } - Err(IrError::Lowering(format!( - "cannot find storage field `{name}` in the current contract" - ))) + Err(IrError::Diagnostic( + edge_diagnostics::Diagnostic::error(format!( + "cannot find storage field `{name}` in the current contract", + )), + )) } } diff --git a/examples/tests/test_full_math.edge b/examples/tests/test_full_math.edge index f448c55..5232fec 100644 --- a/examples/tests/test_full_math.edge +++ b/examples/tests/test_full_math.edge @@ -15,34 +15,34 @@ fn _div_512(a: u256, b: u256, denominator: u256, prod0: u256, prod1: u256) -> (u if remainder > prod0 { borrow2 = 1; } - let p1: u256 = UnsafeSub::unsafe_sub(prod1, borrow2); - let p0: u256 = UnsafeSub::unsafe_sub(prod0, remainder); - let neg_denom: u256 = UnsafeSub::unsafe_sub(0, denominator); + let p1: u256 = prod1.unsafe_sub(borrow2); + let p0: u256 = prod0.unsafe_sub(remainder); + let neg_denom: u256 = 0.unsafe_sub(denominator); let twos: u256 = neg_denom & denominator; let d: u256 = denominator / twos; p0 = p0 / twos; - let twos_inv: u256 = UnsafeAdd::unsafe_add(UnsafeSub::unsafe_sub(0, twos) / twos, 1); - p0 = p0 | UnsafeMul::unsafe_mul(p1, twos_inv); - let inv: u256 = UnsafeMul::unsafe_mul(3, d) ^ 2; - inv = UnsafeMul::unsafe_mul(inv, UnsafeSub::unsafe_sub(2, UnsafeMul::unsafe_mul(d, inv))); - inv = UnsafeMul::unsafe_mul(inv, UnsafeSub::unsafe_sub(2, UnsafeMul::unsafe_mul(d, inv))); - inv = UnsafeMul::unsafe_mul(inv, UnsafeSub::unsafe_sub(2, UnsafeMul::unsafe_mul(d, inv))); - inv = UnsafeMul::unsafe_mul(inv, UnsafeSub::unsafe_sub(2, UnsafeMul::unsafe_mul(d, inv))); - inv = UnsafeMul::unsafe_mul(inv, UnsafeSub::unsafe_sub(2, UnsafeMul::unsafe_mul(d, inv))); - inv = UnsafeMul::unsafe_mul(inv, UnsafeSub::unsafe_sub(2, UnsafeMul::unsafe_mul(d, inv))); - let result: u256 = UnsafeMul::unsafe_mul(p0, inv); + let twos_inv: u256 = (0.unsafe_sub(twos) / twos).unsafe_add(1); + p0 = p0 | p1.unsafe_mul(twos_inv); + let inv: u256 = 3.unsafe_mul(d) ^ 2; + inv = inv.unsafe_mul(2.unsafe_sub(d.unsafe_mul(inv))); + inv = inv.unsafe_mul(2.unsafe_sub(d.unsafe_mul(inv))); + inv = inv.unsafe_mul(2.unsafe_sub(d.unsafe_mul(inv))); + inv = inv.unsafe_mul(2.unsafe_sub(d.unsafe_mul(inv))); + inv = inv.unsafe_mul(2.unsafe_sub(d.unsafe_mul(inv))); + inv = inv.unsafe_mul(2.unsafe_sub(d.unsafe_mul(inv))); + let result: u256 = p0.unsafe_mul(inv); return result; } fn _mul_div(a: u256, b: u256, denominator: u256) -> (u256) { let max_u256: u256 = ~0; let mm: u256 = _mulmod(a, b, max_u256); - let prod0: u256 = UnsafeMul::unsafe_mul(a, b); + let prod0: u256 = a.unsafe_mul(b); let borrow: u256 = 0; if mm < prod0 { borrow = 1; } - let prod1: u256 = UnsafeSub::unsafe_sub(UnsafeSub::unsafe_sub(mm, prod0), borrow); + let prod1: u256 = mm.unsafe_sub(prod0).unsafe_sub(borrow); let result: u256 = 0; if prod1 == 0 { if denominator == 0 { diff --git a/examples/tests/test_inlined_halt.edge b/examples/tests/test_inlined_halt.edge index a716df9..29cfd3a 100644 --- a/examples/tests/test_inlined_halt.edge +++ b/examples/tests/test_inlined_halt.edge @@ -10,14 +10,14 @@ fn _mulmod(a: u256, b: u256, n: u256) -> (u256) { fn _helper(a: u256, b: u256, c: u256, d: u256) -> (u256) { let remainder: u256 = _mulmod(a, b, c); - let result: u256 = UnsafeMul::unsafe_mul(d, remainder); + let result: u256 = d.unsafe_mul(remainder); return result; } fn _compute(a: u256, b: u256, denominator: u256) -> (u256) { let mm: u256 = _mulmod(a, b, ~0); - let prod0: u256 = UnsafeMul::unsafe_mul(a, b); - let prod1: u256 = UnsafeSub::unsafe_sub(mm, prod0); + let prod0: u256 = a.unsafe_mul(b); + let prod1: u256 = mm.unsafe_sub(prod0); let result: u256 = 0; if prod1 == 0 { if denominator == 0 { diff --git a/examples/tests/test_method_on_primitive.edge b/examples/tests/test_method_on_primitive.edge new file mode 100644 index 0000000..a0fd215 --- /dev/null +++ b/examples/tests/test_method_on_primitive.edge @@ -0,0 +1,26 @@ +// test_method_on_primitive.edge — Test trait method calls on primitive types +// +// Tests that trait methods can be called using dot syntax on primitives: +// a.unsafe_add(b) instead of UnsafeAdd::unsafe_add(a, b) + +use std::ops::UnsafeAdd; +use std::ops::UnsafeSub; +use std::ops::UnsafeMul; + +contract MethodOnPrimitive { + pub fn test_unsafe_add(a: u256, b: u256) -> (u256) { + return a.unsafe_add(b); + } + + pub fn test_unsafe_sub(a: u256, b: u256) -> (u256) { + return a.unsafe_sub(b); + } + + pub fn test_unsafe_mul(a: u256, b: u256) -> (u256) { + return a.unsafe_mul(b); + } + + pub fn test_qualified_still_works(a: u256, b: u256) -> (u256) { + return a.unsafe_add(b); + } +} diff --git a/examples/tests/test_signed_widths.edge b/examples/tests/test_signed_widths.edge index 3ef58b5..2a44867 100644 --- a/examples/tests/test_signed_widths.edge +++ b/examples/tests/test_signed_widths.edge @@ -77,17 +77,17 @@ contract TestSignedWidths { // Unsafe add: wraps without reverting pub fn i8_unsafe_add(a: i8, b: i8) -> (i8) { - return UnsafeAdd::unsafe_add(a, b); + return a.unsafe_add(b); } // Unsafe sub: wraps without reverting pub fn i8_unsafe_sub(a: i8, b: i8) -> (i8) { - return UnsafeSub::unsafe_sub(a, b); + return a.unsafe_sub(b); } // Unsafe mul: wraps without reverting pub fn i8_unsafe_mul(a: i8, b: i8) -> (i8) { - return UnsafeMul::unsafe_mul(a, b); + return a.unsafe_mul(b); } // ── Cast signed ↔ unsigned ── diff --git a/examples/tests/test_unsafe_arith.edge b/examples/tests/test_unsafe_arith.edge index ee426d5..9eea86c 100644 --- a/examples/tests/test_unsafe_arith.edge +++ b/examples/tests/test_unsafe_arith.edge @@ -10,33 +10,33 @@ use std::ops::UnsafeMul; contract TestUnsafeArith { // Basic unchecked add pub fn test_unsafe_add() -> (u256) { - return UnsafeAdd::unsafe_add(10, 32); + return 10.unsafe_add(32); } // Basic unchecked sub pub fn test_unsafe_sub() -> (u256) { - return UnsafeSub::unsafe_sub(50, 8); + return 50.unsafe_sub(8); } // Basic unchecked mul pub fn test_unsafe_mul() -> (u256) { - return UnsafeMul::unsafe_mul(6, 7); + return 6.unsafe_mul(7); } // Overflow wraps: MAX_U256 + 1 = 0 pub fn test_add_overflow() -> (u256) { let max: u256 = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; - return UnsafeAdd::unsafe_add(max, 1); + return max.unsafe_add(1); } // Underflow wraps: 0 - 1 = MAX_U256 pub fn test_sub_underflow() -> (u256) { - return UnsafeSub::unsafe_sub(0, 1); + return 0.unsafe_sub(1); } // Mul overflow wraps pub fn test_mul_overflow() -> (u256) { let max: u256 = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; - return UnsafeMul::unsafe_mul(max, 2); + return max.unsafe_mul(2); } } From ef964885c6d216421e54a3ec862cc3a9114702b1 Mon Sep 17 00:00:00 2001 From: brockelmore <31553173+brockelmore@users.noreply.github.com> Date: Wed, 11 Mar 2026 11:23:25 -0700 Subject: [PATCH 2/5] feat: generic Map trait dispatch, default struct derive_slot, auto-import globals - Fix monomorphization cache collision: Map and Map no longer share cache entries (use mangled type names instead of EvmType for cache keys) - Fix composite_base propagation: struct params inferred from type sigs during inlining now get composite_base set to their value (enables field access in trait method bodies) - Add struct param memory allocation for calldata-passed struct params in contract functions (CALLDATACOPY to allocated memory region) - Add default keccak-chained derive_slot for struct types without explicit UniqueSlot impl (Solidity nested mapping convention) - Auto-import std/globals (ops, map, option, result) without explicit use - Move std/ops.edge to std/globals/ops.edge, add globals/map.edge - Add build_type_param_subst with base name fallback for mangled generics - Add tracing at trace level for method dispatch and inline param binding - Egglog tracing now requires verbosity 5 (-vvvvv) instead of 4 - Add Map and Map e2e tests (21 map_std tests total) Co-Authored-By: Claude Opus 4.6 --- bin/edgec/src/main.rs | 4 +- crates/driver/src/compiler.rs | 193 ++++++++- crates/e2e/.gas-snapshot | 12 +- crates/e2e/tests/main.rs | 3 + crates/e2e/tests/suites/map_std_exec.rs | 518 ++++++++++++++++++++++++ crates/evm-tests/src/lib.rs | 7 +- crates/ir/src/lib.rs | 23 +- crates/ir/src/to_egglog/calls.rs | 493 +++++++++++++++++++++- crates/ir/src/to_egglog/control_flow.rs | 2 + crates/ir/src/to_egglog/expr.rs | 82 +++- crates/ir/src/to_egglog/function.rs | 129 +++++- crates/ir/src/to_egglog/mod.rs | 109 ++++- crates/ir/src/to_egglog/pattern.rs | 2 + crates/ir/src/to_egglog/storage.rs | 211 +--------- crates/ir/src/to_egglog/types.rs | 263 +++++++++++- crates/parser/src/parser.rs | 116 +++++- examples/erc20.edge | 4 +- examples/tests/stress_loops.edge | 2 +- examples/tests/stress_storage.edge | 4 +- examples/tests/test_erc20.edge | 4 +- examples/tests/test_map_std.edge | 120 ++++++ examples/tests/test_mappings.edge | 6 +- examples/tests/test_storage_heavy.edge | 4 +- examples/tokens/erc20.edge | 4 +- examples/tokens/erc721.edge | 2 +- std/access/roles.edge | 6 +- std/finance/amm.edge | 4 +- std/finance/multisig.edge | 14 +- std/finance/staking.edge | 8 +- std/globals/map.edge | 87 ++++ std/{ => globals}/ops.edge | 21 + std/globals/option.edge | 1 + std/globals/result.edge | 1 + std/patterns/factory.edge | 4 +- std/patterns/timelock.edge | 6 +- std/tokens/erc1155.edge | 4 +- std/tokens/erc20.edge | 4 +- std/tokens/weth.edge | 6 +- 38 files changed, 2168 insertions(+), 315 deletions(-) create mode 100644 crates/e2e/tests/suites/map_std_exec.rs create mode 100644 examples/tests/test_map_std.edge create mode 100644 std/globals/map.edge rename std/{ => globals}/ops.edge (67%) create mode 100644 std/globals/option.edge create mode 100644 std/globals/result.edge diff --git a/bin/edgec/src/main.rs b/bin/edgec/src/main.rs index 68f129f..86c6899 100644 --- a/bin/edgec/src/main.rs +++ b/bin/edgec/src/main.rs @@ -19,8 +19,8 @@ fn main() -> Result<()> { if let Some(level) = level { use tracing_subscriber::EnvFilter; - // Egglog is extremely noisy, suppress it unless TRACE level - let egglog_level = if level >= Level::TRACE { + // Egglog is extremely noisy — only enable at verbosity 5+ (-vvvvv) + let egglog_level = if cli.verbose >= 5 { "trace" } else { "warn" diff --git a/crates/driver/src/compiler.rs b/crates/driver/src/compiler.rs index bf9f5cd..332ec86 100644 --- a/crates/driver/src/compiler.rs +++ b/crates/driver/src/compiler.rs @@ -110,6 +110,15 @@ impl Compiler { .render_to_string(&path, &self.session.source) } + /// Parse and resolve imports, returning the preprocessed AST. + /// Useful for tests that need to control IR/codegen optimization levels separately. + pub fn parse_and_resolve(&mut self) -> Result { + let _tokens = self.lex()?; + let mut ast = self.parse()?; + self.resolve_imports(&mut ast)?; + Ok(ast) + } + /// Run the compilation pipeline pub fn compile(&mut self) -> Result { tracing::info!("Compiling {:?}", self.session.config.input_file); @@ -357,7 +366,7 @@ impl Compiler { fn parse(&mut self) -> Result { let mut parser = Parser::new(&self.session.source).map_err(|e| { self.session - .emit_error(Diagnostic::error(format!("parse error: {e}"))); + .emit_error(Self::parse_error_to_diagnostic(&e)); CompileError::ParseErrors })?; @@ -365,13 +374,44 @@ impl Compiler { Ok(program) => Ok(program), Err(e) => { self.session - .emit_error(Diagnostic::error(format!("parse error: {e}"))); + .emit_error(Self::parse_error_to_diagnostic(&e)); self.session.report_diagnostics(); Err(CompileError::ParseErrors) } } } + /// Convert a `ParseError` into a `Diagnostic` with proper span labels. + fn parse_error_to_diagnostic(e: &edge_parser::ParseError) -> Diagnostic { + use edge_parser::ParseError; + match e { + ParseError::UnexpectedToken { + found, + expected, + span, + } => Diagnostic::error(format!("expected {expected}, found {found}")) + .with_label(span.clone(), format!("expected {expected}")), + ParseError::InvalidTypeSig { message, span } => { + Diagnostic::error(format!("invalid type: {message}")) + .with_label(span.clone(), message.clone()) + } + ParseError::InvalidExpr { message, span } => { + Diagnostic::error(format!("invalid expression: {message}")) + .with_label(span.clone(), message.clone()) + } + ParseError::InvalidStmt { message, span } => { + Diagnostic::error(format!("invalid statement: {message}")) + .with_label(span.clone(), message.clone()) + } + ParseError::InvalidPattern { message, span } => { + Diagnostic::error(format!("invalid pattern: {message}")) + .with_label(span.clone(), message.clone()) + } + ParseError::UnexpectedEof => Diagnostic::error("unexpected end of file"), + ParseError::LexerError(msg) => Diagnostic::error(format!("lexer error: {msg}")), + } + } + /// Resolve `use std::...` imports by locating source for each imported module — /// first from an explicit filesystem override, then from the stdlib embedded in /// the binary — and merging their top-level items into the program AST. @@ -382,6 +422,10 @@ impl Compiler { /// 2. Embedded sources baked into the binary at compile time via `build.rs` /// (works on any machine with no extra setup). fn resolve_imports(&mut self, ast: &mut Program) -> Result<(), CompileError> { + // Auto-import globals: ops, map, option, result. + // These are always available without explicit `use` statements. + self.auto_import_globals(ast)?; + // Collect std imports from the AST. // Build a full path-segments list by combining intermediate `segments` with the final // `path` identifier. For example: @@ -516,6 +560,104 @@ impl Compiler { Ok(()) } + /// Auto-import all `std/globals/*.edge` files — these are always available + /// without explicit `use` statements. Prepends their declarations (types, + /// traits, impls, functions) to the AST. + fn auto_import_globals(&mut self, ast: &mut Program) -> Result<(), CompileError> { + // Order matters: ops first (trait defs), then map (uses ops traits). + let global_keys = ["globals/ops", "globals/option", "globals/result", "globals/map"]; + let mut new_stmts: Vec = Vec::new(); + + // Canonicalize the explicit override path once (if provided). + let explicit_std_path: Option = + self.session.config.std_path.as_ref().and_then(|p| { + let canon = std::fs::canonicalize(p).unwrap_or_else(|_| p.clone()); + if canon.is_dir() { + Some(canon) + } else { + None + } + }); + + for key in &global_keys { + let segments: Vec = key.split('/').map(String::from).collect(); + let source = if let Some(ref std_path) = explicit_std_path { + Self::try_read_from_fs(std_path, &segments) + .or_else(|| Self::try_read_from_embedded(&segments).map(String::from)) + } else { + Self::try_read_from_embedded(&segments).map(String::from) + }; + + let Some(source) = source else { + // Globals not available (e.g., downstream consumer without std/). + continue; + }; + + let mut parser = Parser::new(&source).map_err(|e| { + self.session.emit_error(Diagnostic::error(format!( + "parse error in globals `{key}`: {e}" + ))); + CompileError::ParseErrors + })?; + + let program = parser.parse().map_err(|e| { + self.session.emit_error(Diagnostic::error(format!( + "parse error in globals `{key}`: {e}" + ))); + self.session.report_diagnostics(); + CompileError::ParseErrors + })?; + + // Include everything except ModuleImport/ModuleDecl (internal imports). + for stmt in program.stmts { + if !matches!( + stmt, + edge_ast::Stmt::ModuleImport(_) | edge_ast::Stmt::ModuleDecl(_) + ) { + new_stmts.push(stmt); + } + } + } + + if !new_stmts.is_empty() { + // Collect names defined in the user's file so globals don't shadow them. + // Like Rust's prelude: local definitions take priority over auto-imports. + let user_defined: std::collections::HashSet = ast + .stmts + .iter() + .filter_map(|stmt| match stmt { + edge_ast::Stmt::TypeAssign(td, _, _) => Some(td.name.name.clone()), + edge_ast::Stmt::TraitDecl(tr, _) => Some(tr.name.name.clone()), + edge_ast::Stmt::FnAssign(fd, _) | edge_ast::Stmt::ComptimeFn(fd, _) => { + Some(fd.name.name.clone()) + } + _ => None, + }) + .collect(); + + // Filter out global statements whose name collides with user definitions. + new_stmts.retain(|stmt| { + let name = match stmt { + edge_ast::Stmt::TypeAssign(td, _, _) => Some(&td.name.name), + edge_ast::Stmt::TraitDecl(tr, _) => Some(&tr.name.name), + edge_ast::Stmt::FnAssign(fd, _) | edge_ast::Stmt::ComptimeFn(fd, _) => { + Some(&fd.name.name) + } + _ => None, + }; + if let Some(n) = name { + !user_defined.contains(n) + } else { + true + } + }); + + new_stmts.append(&mut ast.stmts); + ast.stmts = new_stmts; + } + Ok(()) + } + /// Resolve a set of import path segments to a `(module_key, source)` pair. /// /// Tries, in order: @@ -588,6 +730,53 @@ impl Compiler { } } + // Before giving up, check if this import points to a globals module + // (e.g., `use std::ops::Add` → file "ops" not found, but "globals/ops" exists). + // If so, the content is already auto-imported — just return it. + // + // Try two forms: + // 1. Full path: ["globals"] + segments (e.g., "globals/ops/Add") + // 2. Symbol-level: ["globals"] + segments[..n-1], symbol = segments[n-1] + // (e.g., "globals/ops" with symbol "Add") + { + // Form 1: full path with globals prefix + let fallback_segments: Vec = std::iter::once("globals".to_string()) + .chain(segments.iter().cloned()) + .collect(); + + if let Some(ref std_path) = explicit_std_path { + if let Some(source) = Self::try_read_from_fs(std_path, &fallback_segments) { + let key = fallback_segments.join("/"); + return Ok((key, source, None)); + } + } + + if let Some(source) = Self::try_read_from_embedded(&fallback_segments) { + let key = fallback_segments.join("/"); + return Ok((key, source.to_string(), None)); + } + + // Form 2: strip last segment as symbol name within globals file + if segments.len() > 1 { + let symbol = segments.last().unwrap().clone(); + let file_fallback: Vec = std::iter::once("globals".to_string()) + .chain(segments[..segments.len() - 1].iter().cloned()) + .collect(); + + if let Some(ref std_path) = explicit_std_path { + if let Some(source) = Self::try_read_from_fs(std_path, &file_fallback) { + let key = file_fallback.join("/"); + return Ok((key, source, Some(symbol))); + } + } + + if let Some(source) = Self::try_read_from_embedded(&file_fallback) { + let key = file_fallback.join("/"); + return Ok((key, source.to_string(), Some(symbol))); + } + } + } + // Nothing found — emit a helpful error. let module_path = segments.join("::"); self.session.emit_error(Diagnostic::error(format!( diff --git a/crates/e2e/.gas-snapshot b/crates/e2e/.gas-snapshot index 1ebf971..6010a33 100644 --- a/crates/e2e/.gas-snapshot +++ b/crates/e2e/.gas-snapshot @@ -68,14 +68,14 @@ test_loop_storage::get_total(), 2255, 2270, 2270, 2270 test_loop_storage::read_write_loop(uint256), 44981, 44996, 44996, 44996 test_loop_storage::reset(), 4757, 4775, 4775, 4775 test_mappings::counter_get(address), 2287, 2302, 2302, 2302 -test_mappings::counter_inc(address), 22446, 22453, 22453, 22453 -test_mappings::map_add(address,uint256), 5346, 5350, 5350, 5350 +test_mappings::counter_inc(address), 22449, 22456, 22456, 22456 +test_mappings::map_add(address,uint256), 5349, 5353, 5353, 5353 test_mappings::map_get(address), 2235, 2250, 2250, 2250 -test_mappings::map_set(address,uint256), 22332, 22345, 22345, 22345 +test_mappings::map_set(address,uint256), 22335, 22348, 22348, 22348 test_mappings::nested_get(address,address), 2449, 2455, 2455, 2455 -test_mappings::nested_set(address,address,uint256), 22444, 22448, 22448, 22448 -test_mappings::nested_two_spenders(address,address,address,uint256,uint256), 44693, 44679, 44679, 44679 -test_mappings::two_keys(address,address,uint256,uint256), 44473, 44474, 44474, 44474 +test_mappings::nested_set(address,address,uint256), 22447, 22451, 22451, 22451 +test_mappings::nested_two_spenders(address,address,address,uint256,uint256), 44699, 44685, 44685, 44685 +test_mappings::two_keys(address,address,uint256,uint256), 44479, 44480, 44480, 44480 test_merkle::hash_two(bytes32,bytes32), 516, 516, 516, 516 test_merkle::verify(bytes32,bytes32,bytes32[4],uint256), 1530, 1530, 1530, 1530 test_packed_storage::store_and_read_b(), 22327, 22289, 22273, 22273 diff --git a/crates/e2e/tests/main.rs b/crates/e2e/tests/main.rs index aae1e77..d440441 100644 --- a/crates/e2e/tests/main.rs +++ b/crates/e2e/tests/main.rs @@ -54,6 +54,9 @@ mod utils_exec; #[path = "suites/warnings.rs"] mod warnings; +#[path = "suites/map_std_exec.rs"] +mod map_std_exec; + #[path = "suites/int_widths_exec.rs"] mod int_widths_exec; #[path = "suites/large_int_literals.rs"] diff --git a/crates/e2e/tests/suites/map_std_exec.rs b/crates/e2e/tests/suites/map_std_exec.rs new file mode 100644 index 0000000..06ea7b4 --- /dev/null +++ b/crates/e2e/tests/suites/map_std_exec.rs @@ -0,0 +1,518 @@ +#![allow(missing_docs)] + +//! Execution-level tests for the std Map type. +//! +//! Tests compile test_map_std.edge, deploy on in-memory revm, and verify +//! basic Map get/set, index operators, direct custom storage, and +//! Map with user-defined Sload/Sstore impls. + +use crate::helpers::*; + +const CONTRACT: &str = "examples/tests/test_map_std.edge"; + +/// Pack two u128 values into a 32-byte big-endian representation: (a << 128) | b +fn pack_u128_pair(a: u128, b: u128) -> [u8; 32] { + let mut out = [0u8; 32]; + out[0..16].copy_from_slice(&a.to_be_bytes()); + out[16..32].copy_from_slice(&b.to_be_bytes()); + out +} + +// ============================================================================= +// Direct custom storage: set_custom(u128,u128) / get_custom() +// ============================================================================= + +#[test] +fn test_custom_storage_initially_zero() { + let bc = compile_contract(CONTRACT); + let mut evm = EvmHandle::new(bc); + // get_custom() returns CustomSStore — struct with 3 fields, so 3 words + // But actually the contract returns `custom` which is a storage field. + // CustomSStore has 3 fields: ignored(u256), packed_a(u128), packed_b(u128) + // When returned, it should be the packed u256 from storage. + let r = evm.call(calldata(selector("get_custom()"), &[])); + assert!(r.success, "get_custom() reverted"); + assert_eq!(decode_u256(&r.output), 0, "custom should start at 0"); +} + +#[test] +fn test_custom_storage_set_then_get() { + let bc = compile_contract(CONTRACT); + let mut evm = EvmHandle::new(bc); + + // set_custom(a=5, b=10) — packs as (5 << 128) | 10 + let r = evm.call(calldata( + selector("set_custom(uint128,uint128)"), + &[encode_u256(5), encode_u256(10)], + )); + assert!(r.success, "set_custom(5, 10) reverted"); + + let r = evm.call(calldata(selector("get_custom()"), &[])); + assert!(r.success, "get_custom() reverted"); + // The stored value is the packed combo: (packed_a << 128) | packed_b + // But the return type is CustomSStore, which goes through Sload. + // CustomSStore::sload reads packed_combo, then returns struct fields. + // The return will be the raw storage value or unpacked fields depending + // on how the compiler handles struct returns. + // For now just check it doesn't revert and returns non-zero. + assert!(r.output.len() >= 32, "should return at least 32 bytes"); +} + +// ============================================================================= +// Basic Map — get/set +// ============================================================================= + +#[test] +fn test_basic_map_get_initially_zero() { + let bc = compile_contract(CONTRACT); + let mut evm = EvmHandle::new(bc); + let r = evm.call(calldata( + selector("get_basic(uint256)"), + &[encode_u256(42)], + )); + assert!(r.success, "get_basic(42) reverted"); + assert_eq!(decode_u256(&r.output), 0, "unset key should return 0"); +} + +#[test] +fn test_basic_map_set_then_get() { + let bc = compile_contract(CONTRACT); + let mut evm = EvmHandle::new(bc); + + let r = evm.call(calldata( + selector("set_basic(uint256,uint256)"), + &[encode_u256(1), encode_u256(999)], + )); + assert!(r.success, "set_basic(1, 999) reverted"); + + let r = evm.call(calldata( + selector("get_basic(uint256)"), + &[encode_u256(1)], + )); + assert!(r.success, "get_basic(1) reverted"); + assert_eq!(decode_u256(&r.output), 999, "get_basic(1) should be 999"); +} + +#[test] +fn test_basic_map_different_keys_independent() { + let bc = compile_contract(CONTRACT); + let mut evm = EvmHandle::new(bc); + + let r = evm.call(calldata( + selector("set_basic(uint256,uint256)"), + &[encode_u256(10), encode_u256(100)], + )); + assert!(r.success, "set_basic(10, 100) reverted"); + + let r = evm.call(calldata( + selector("set_basic(uint256,uint256)"), + &[encode_u256(20), encode_u256(200)], + )); + assert!(r.success, "set_basic(20, 200) reverted"); + + let r = evm.call(calldata( + selector("get_basic(uint256)"), + &[encode_u256(10)], + )); + assert!(r.success); + assert_eq!(decode_u256(&r.output), 100); + + let r = evm.call(calldata( + selector("get_basic(uint256)"), + &[encode_u256(20)], + )); + assert!(r.success); + assert_eq!(decode_u256(&r.output), 200); +} + +#[test] +fn test_basic_map_overwrite() { + let bc = compile_contract(CONTRACT); + let mut evm = EvmHandle::new(bc); + + let r = evm.call(calldata( + selector("set_basic(uint256,uint256)"), + &[encode_u256(5), encode_u256(111)], + )); + assert!(r.success); + + let r = evm.call(calldata( + selector("set_basic(uint256,uint256)"), + &[encode_u256(5), encode_u256(222)], + )); + assert!(r.success); + + let r = evm.call(calldata( + selector("get_basic(uint256)"), + &[encode_u256(5)], + )); + assert!(r.success); + assert_eq!(decode_u256(&r.output), 222, "overwritten value should be 222"); +} + +// ============================================================================= +// Index operator — get_basic_by_indexable / set_basic_by_indexable +// ============================================================================= + +#[test] +fn test_basic_map_index_get() { + let bc = compile_contract(CONTRACT); + let mut evm = EvmHandle::new(bc); + + // Set via .set() + let r = evm.call(calldata( + selector("set_basic(uint256,uint256)"), + &[encode_u256(7), encode_u256(777)], + )); + assert!(r.success, "set_basic reverted"); + + // Read via index operator + let r = evm.call(calldata( + selector("get_basic_by_indexable(uint256)"), + &[encode_u256(7)], + )); + assert!(r.success, "get_basic_by_indexable reverted"); + assert_eq!(decode_u256(&r.output), 777); +} + +#[test] +fn test_basic_map_index_set() { + let bc = compile_contract(CONTRACT); + let mut evm = EvmHandle::new(bc); + + // Set via index operator + let r = evm.call(calldata( + selector("set_basic_by_indexable(uint256,uint256)"), + &[encode_u256(3), encode_u256(333)], + )); + assert!(r.success, "set_basic_by_indexable reverted"); + + // Read via .get() + let r = evm.call(calldata( + selector("get_basic(uint256)"), + &[encode_u256(3)], + )); + assert!(r.success, "get_basic reverted"); + assert_eq!(decode_u256(&r.output), 333); +} + +#[test] +fn test_basic_map_index_interop() { + let bc = compile_contract(CONTRACT); + let mut evm = EvmHandle::new(bc); + + // Set via index, read via index + let r = evm.call(calldata( + selector("set_basic_by_indexable(uint256,uint256)"), + &[encode_u256(99), encode_u256(9999)], + )); + assert!(r.success); + + let r = evm.call(calldata( + selector("get_basic_by_indexable(uint256)"), + &[encode_u256(99)], + )); + assert!(r.success); + assert_eq!(decode_u256(&r.output), 9999); + + // Also readable via .get() + let r = evm.call(calldata( + selector("get_basic(uint256)"), + &[encode_u256(99)], + )); + assert!(r.success); + assert_eq!(decode_u256(&r.output), 9999, ".get and index should read same slot"); +} + +// ============================================================================= +// Custom Sload/Sstore Map — Map +// get_custom(uint256), get_custom_by_indexable(uint256), +// set_custom(uint256, CustomSStore), set_custom_by_indexable(uint256, CustomSStore) +// ============================================================================= + +// Note: CustomSStore.sstore packs (packed_a << 128) | packed_b into a single u256. +// CustomSStore.sload reads that u256 and unpacks it back. +// The get_custom(uint256) return type is (u256), so it returns the raw packed value. + +#[test] +fn test_custom_map_get_initially_zero() { + let bc = compile_contract(CONTRACT); + let mut evm = EvmHandle::new(bc); + let r = evm.call(calldata( + selector("get_custom(uint256)"), + &[encode_u256(1)], + )); + assert!(r.success, "get_custom(1) reverted"); + assert_eq!(decode_u256(&r.output), 0, "unset custom map key should be 0"); +} + +#[test] +fn test_custom_map_get_by_indexable_initially_zero() { + let bc = compile_contract(CONTRACT); + let mut evm = EvmHandle::new(bc); + let r = evm.call(calldata( + selector("get_custom_by_indexable(uint256)"), + &[encode_u256(1)], + )); + assert!(r.success, "get_custom_by_indexable(1) reverted"); + assert_eq!(decode_u256(&r.output), 0); +} + +// Note: set_custom(uint256, CustomSStore) takes a struct with 3 fields as the +// second arg. ABI-encoding: 3 words (ignored, packed_a, packed_b) = 4 words total +// with the key. But the ABI signature for selector hashing depends on how Edge +// encodes struct params. We'll try the natural encoding. +// CustomSStore = { ignored: u256, packed_a: u128, packed_b: u128 } +// ABI sig might be: set_custom(uint256,uint256,uint128,uint128) or +// set_custom(uint256,(uint256,uint128,uint128)) + +// For now, test the functions that take simple u256 args (get_custom, get_custom_by_indexable) +// and verify they work after setting values via the basic u256 map functions. + +// ============================================================================= +// Double custom: Map +// CustomHash key uses user-defined UniqueSlot::derive_slot +// CustomSStore value uses user-defined Sload/Sstore +// ============================================================================= + +#[test] +fn test_double_custom_get_initially_zero() { + let bc = compile_contract(CONTRACT); + let mut evm = EvmHandle::new(bc); + // get_double_custom(a=1, b=2) — key is CustomHash{a:1, b:2} + let r = evm.call(calldata( + selector("get_double_custom(uint128,uint128)"), + &[encode_u256(1), encode_u256(2)], + )); + assert!(r.success, "get_double_custom(1,2) reverted"); + assert_eq!(decode_u256(&r.output), 0, "unset double custom key should return 0"); +} + +#[test] +fn test_double_custom_set_then_get() { + let bc = compile_contract(CONTRACT); + let mut evm = EvmHandle::new(bc); + + // set_double_custom(a=1, b=2, val_a=100, val_b=200) + let r = evm.call(calldata( + selector("set_double_custom(uint128,uint128,uint128,uint128)"), + &[encode_u256(1), encode_u256(2), encode_u256(100), encode_u256(200)], + )); + assert!(r.success, "set_double_custom reverted"); + + // get_double_custom(a=1, b=2) — should return packed (100 << 128) | 200 + let r = evm.call(calldata( + selector("get_double_custom(uint128,uint128)"), + &[encode_u256(1), encode_u256(2)], + )); + assert!(r.success, "get_double_custom reverted"); + assert!(r.output.len() >= 32); + // Packed as (val_a << 128) | val_b in a u256 + // val_a=100 in bytes 0..16, val_b=200 in bytes 16..32 + let packed = &r.output[0..32]; + assert!(packed.iter().any(|&b| b != 0), "stored value should be non-zero"); +} + +#[test] +fn test_double_custom_different_keys_independent() { + let bc = compile_contract(CONTRACT); + let mut evm = EvmHandle::new(bc); + + // Set key (1, 2) → val (10, 20) + let r = evm.call(calldata( + selector("set_double_custom(uint128,uint128,uint128,uint128)"), + &[encode_u256(1), encode_u256(2), encode_u256(10), encode_u256(20)], + )); + assert!(r.success); + + // Set key (3, 4) → val (30, 40) + let r = evm.call(calldata( + selector("set_double_custom(uint128,uint128,uint128,uint128)"), + &[encode_u256(3), encode_u256(4), encode_u256(30), encode_u256(40)], + )); + assert!(r.success); + + // Read key (1, 2) — should get val (10, 20) packed + let r = evm.call(calldata( + selector("get_double_custom(uint128,uint128)"), + &[encode_u256(1), encode_u256(2)], + )); + assert!(r.success); + // Expected packed value: (10 << 128) | 20 + // In big-endian 32 bytes: bytes[0..16] = 10, bytes[16..32] = 20 + let expected_1_2 = pack_u128_pair(10, 20); + assert_eq!(&r.output[0..32], &expected_1_2[..], "key (1,2) should have val (10,20)"); + + // Read key (3, 4) — should get val (30, 40) packed + let r = evm.call(calldata( + selector("get_double_custom(uint128,uint128)"), + &[encode_u256(3), encode_u256(4)], + )); + assert!(r.success); + let expected_3_4 = pack_u128_pair(30, 40); + assert_eq!(&r.output[0..32], &expected_3_4[..], "key (3,4) should have val (30,40)"); +} + +#[test] +fn test_double_custom_overwrite() { + let bc = compile_contract(CONTRACT); + let mut evm = EvmHandle::new(bc); + + // Set key (5, 6) → val (50, 60) + let r = evm.call(calldata( + selector("set_double_custom(uint128,uint128,uint128,uint128)"), + &[encode_u256(5), encode_u256(6), encode_u256(50), encode_u256(60)], + )); + assert!(r.success); + + // Overwrite key (5, 6) → val (55, 66) + let r = evm.call(calldata( + selector("set_double_custom(uint128,uint128,uint128,uint128)"), + &[encode_u256(5), encode_u256(6), encode_u256(55), encode_u256(66)], + )); + assert!(r.success); + + // Read key (5, 6) + let r = evm.call(calldata( + selector("get_double_custom(uint128,uint128)"), + &[encode_u256(5), encode_u256(6)], + )); + assert!(r.success); + let expected = pack_u128_pair(55, 66); + assert_eq!(&r.output[0..32], &expected[..], "overwritten value should be (55,66)"); +} + +// ============================================================================= +// Default derive_slot: Map +// DefaultKey has no UniqueSlot impl — compiler provides keccak-chained default. +// Slot = keccak256(y . keccak256(x . base_slot)) +// ============================================================================= + +#[test] +fn test_default_key_get_initially_zero() { + let bc = compile_contract(CONTRACT); + let mut evm = EvmHandle::new(bc); + let r = evm.call(calldata( + selector("get_default_key(uint256,uint256)"), + &[encode_u256(1), encode_u256(2)], + )); + assert!(r.success, "get_default_key(1,2) reverted"); + assert_eq!(decode_u256(&r.output), 0, "unset key should return 0"); +} + +#[test] +fn test_default_key_set_then_get() { + let bc = compile_contract(CONTRACT); + let mut evm = EvmHandle::new(bc); + + let r = evm.call(calldata( + selector("set_default_key(uint256,uint256,uint256)"), + &[encode_u256(10), encode_u256(20), encode_u256(999)], + )); + assert!(r.success, "set_default_key reverted"); + + let r = evm.call(calldata( + selector("get_default_key(uint256,uint256)"), + &[encode_u256(10), encode_u256(20)], + )); + assert!(r.success, "get_default_key reverted"); + assert_eq!(decode_u256(&r.output), 999, "should read back 999"); +} + +#[test] +fn test_default_key_different_keys_independent() { + let bc = compile_contract(CONTRACT); + let mut evm = EvmHandle::new(bc); + + // Set (1, 2) → 100 + let r = evm.call(calldata( + selector("set_default_key(uint256,uint256,uint256)"), + &[encode_u256(1), encode_u256(2), encode_u256(100)], + )); + assert!(r.success); + + // Set (3, 4) → 200 + let r = evm.call(calldata( + selector("set_default_key(uint256,uint256,uint256)"), + &[encode_u256(3), encode_u256(4), encode_u256(200)], + )); + assert!(r.success); + + // Read (1, 2) — should be 100 + let r = evm.call(calldata( + selector("get_default_key(uint256,uint256)"), + &[encode_u256(1), encode_u256(2)], + )); + assert!(r.success); + assert_eq!(decode_u256(&r.output), 100); + + // Read (3, 4) — should be 200 + let r = evm.call(calldata( + selector("get_default_key(uint256,uint256)"), + &[encode_u256(3), encode_u256(4)], + )); + assert!(r.success); + assert_eq!(decode_u256(&r.output), 200); +} + +#[test] +fn test_default_key_field_order_matters() { + let bc = compile_contract(CONTRACT); + let mut evm = EvmHandle::new(bc); + + // Set (1, 2) → 111 + let r = evm.call(calldata( + selector("set_default_key(uint256,uint256,uint256)"), + &[encode_u256(1), encode_u256(2), encode_u256(111)], + )); + assert!(r.success); + + // Read (2, 1) — should be 0, NOT 111 (field order matters in keccak chain) + let r = evm.call(calldata( + selector("get_default_key(uint256,uint256)"), + &[encode_u256(2), encode_u256(1)], + )); + assert!(r.success); + assert_eq!( + decode_u256(&r.output), + 0, + "swapped fields should map to different slot" + ); +} + +#[test] +fn test_default_key_overwrite() { + let bc = compile_contract(CONTRACT); + let mut evm = EvmHandle::new(bc); + + let r = evm.call(calldata( + selector("set_default_key(uint256,uint256,uint256)"), + &[encode_u256(5), encode_u256(6), encode_u256(50)], + )); + assert!(r.success); + + let r = evm.call(calldata( + selector("set_default_key(uint256,uint256,uint256)"), + &[encode_u256(5), encode_u256(6), encode_u256(60)], + )); + assert!(r.success); + + let r = evm.call(calldata( + selector("get_default_key(uint256,uint256)"), + &[encode_u256(5), encode_u256(6)], + )); + assert!(r.success); + assert_eq!(decode_u256(&r.output), 60, "overwritten value should be 60"); +} + +// ============================================================================= +// Unknown selector +// ============================================================================= + +#[test] +fn test_map_std_unknown_selector_reverts() { + let bc = compile_contract(CONTRACT); + let mut evm = EvmHandle::new(bc); + let r = evm.call(vec![0xde, 0xad, 0xbe, 0xef]); + assert!(!r.success, "unknown selector should revert"); +} diff --git a/crates/evm-tests/src/lib.rs b/crates/evm-tests/src/lib.rs index 508a311..28436c1 100644 --- a/crates/evm-tests/src/lib.rs +++ b/crates/evm-tests/src/lib.rs @@ -289,9 +289,10 @@ pub fn compile_edge_split( bytecode_opt_level: u8, optimize_for: edge_ir::OptimizeFor, ) -> Vec { - let source = std::fs::read_to_string(path).expect("failed to read source"); - let mut parser = edge_parser::Parser::new(&source).expect("failed to create parser"); - let ast = parser.parse().expect("parse failed"); + let mut config = CompilerConfig::new(PathBuf::from(path)); + config.emit = EmitKind::Bytecode; + let mut compiler = Compiler::new(config).expect("failed to create compiler"); + let ast = compiler.parse_and_resolve().expect("parse failed"); let ir_program = edge_ir::lower_and_optimize(&ast, ir_opt_level, optimize_for) .expect("IR optimization failed"); edge_codegen::compile(&ir_program, bytecode_opt_level, optimize_for).expect("codegen failed") diff --git a/crates/ir/src/lib.rs b/crates/ir/src/lib.rs index ae7a202..195e78f 100644 --- a/crates/ir/src/lib.rs +++ b/crates/ir/src/lib.rs @@ -551,7 +551,28 @@ mod tests { fn test_egglog_roundtrip_erc20() { let source = std::fs::read_to_string("../../examples/erc20.edge").unwrap(); let mut parser = edge_parser::Parser::new(&source).unwrap(); - let ast = parser.parse().unwrap(); + let mut ast = parser.parse().unwrap(); + + // Import globals (ops, map, etc.) the same way the driver does + let global_files = [ + "globals/ops", + "globals/option", + "globals/result", + "globals/map", + ]; + for key in &global_files { + let path = format!("../../std/{}.edge", key); + if let Ok(src) = std::fs::read_to_string(&path) { + if let Ok(mut p) = edge_parser::Parser::new(&src) { + if let Ok(globals_ast) = p.parse() { + // Prepend globals statements + let mut new_stmts = globals_ast.stmts; + new_stmts.append(&mut ast.stmts); + ast.stmts = new_stmts; + } + } + } + } let mut lowering = to_egglog::AstToEgglog::new(); let ir_program = lowering.lower_program(&ast).unwrap(); diff --git a/crates/ir/src/to_egglog/calls.rs b/crates/ir/src/to_egglog/calls.rs index d232137..16cf81d 100644 --- a/crates/ir/src/to_egglog/calls.rs +++ b/crates/ir/src/to_egglog/calls.rs @@ -1,5 +1,8 @@ //! Function call lowering: call resolution, inlining, builtin calls. +use std::collections::HashMap; +use std::rc::Rc; + use super::{AstToEgglog, FreeFnInfo, Scope, VarBinding}; use crate::{ ast_helpers, @@ -60,13 +63,18 @@ impl AstToEgglog { // Check for qualified trait/type call: Path(["Type", "method"]) if let edge_ast::Expr::Path(components, _) = callee { if components.len() == 2 { - let type_or_trait = &components[0].name; + // Resolve type parameter substitutions (e.g., V → u256 inside Map methods) + let resolved_type = self.type_param_subst + .get(&components[0].name) + .cloned() + .unwrap_or_else(|| components[0].name.clone()); + let type_or_trait = &resolved_type; let method_name = &components[1].name; let method_span = &components[1].span; // Check inherent methods: Type::method(receiver, args...) - if self.inherent_methods.contains_key(type_or_trait) { + if self.find_inherent_method(type_or_trait, method_name).is_some() { return self.lower_qualified_method_call( type_or_trait, method_name, @@ -86,6 +94,39 @@ impl AstToEgglog { method_span, ); } + + // Check primitive type qualified calls: u256::sload(slot), etc. + // This handles resolved type parameters like V::sload() where V = u256. + if Self::is_primitive_type(type_or_trait) { + return self.lower_qualified_trait_call( + type_or_trait, + method_name, + args, + method_span, + ); + } + + // Check trait impls for non-primitive types: Map::sload(slot), etc. + // Directly look up and inline the method from the type's trait impls. + if let Some((fn_decl, body)) = self.find_trait_method_for_type(type_or_trait, method_name) { + let params: Vec<(String, edge_ast::ty::TypeSig)> = fn_decl + .params + .iter() + .map(|(id, ty)| (id.name.clone(), ty.clone())) + .collect(); + return self.inline_function_call(¶ms, &body, args); + } + // Also check inherent methods on the type + if let Some(method) = self.find_inherent_method(type_or_trait, method_name) { + let fn_decl = method.fn_decl.clone(); + let body = method.body; + let params: Vec<(String, edge_ast::ty::TypeSig)> = fn_decl + .params + .iter() + .map(|(id, ty)| (id.name.clone(), ty.clone())) + .collect(); + return self.inline_function_call(¶ms, &body, args); + } } } @@ -171,8 +212,17 @@ impl AstToEgglog { ) -> Result { // Determine receiver type from scope bindings let receiver_type = self.infer_receiver_type(receiver); + let receiver_type_args = self.infer_receiver_type_args(receiver); + tracing::trace!( + "lower_method_call: .{}(), receiver_type={:?}", + method_name, + receiver_type + ); if let Some(ref type_name) = receiver_type { + // Build type param substitution map for generic types + let type_param_subst = self.build_type_param_subst(type_name, &receiver_type_args); + // Check inherent methods first if let Some(method) = self.find_inherent_method(type_name, method_name) { let fn_decl = method.fn_decl.clone(); @@ -185,7 +235,11 @@ impl AstToEgglog { .iter() .map(|(id, ty)| (id.name.clone(), ty.clone())) .collect(); - return self.inline_function_call(¶ms, &body, &all_args); + // Set type param substitutions for generic method bodies + let old_subst = std::mem::replace(&mut self.type_param_subst, type_param_subst.clone()); + let result = self.inline_function_call(¶ms, &body, &all_args); + self.type_param_subst = old_subst; + return result; } // Check trait impls @@ -197,7 +251,10 @@ impl AstToEgglog { .iter() .map(|(id, ty)| (id.name.clone(), ty.clone())) .collect(); - return self.inline_function_call(¶ms, &body, &all_args); + let old_subst = std::mem::replace(&mut self.type_param_subst, type_param_subst); + let result = self.inline_function_call(¶ms, &body, &all_args); + self.type_param_subst = old_subst; + return result; } // Check compiler-provided trait methods for primitive types @@ -215,6 +272,41 @@ impl AstToEgglog { let rhs = self.lower_expr(&args[0])?; return Ok(ast_helpers::bop(op, lhs, rhs)); } + + // Check compiler-provided stateful methods (derive_slot, sload, sstore) + { + let recv_ir = self.lower_expr(receiver)?; + let args_ir: Vec = args + .iter() + .map(|a| self.lower_expr(a)) + .collect::>()?; + if let Some(result) = + self.compiler_provided_stateful_method(method_name, Some(recv_ir), &args_ir) + { + return Ok(result); + } + } + } + + // Default derive_slot for struct types without explicit UniqueSlot impl. + // Chains keccak256 over each field like Solidity nested mappings: + // slot = keccak256(field_0 . base_slot) + // slot = keccak256(field_1 . slot) + // ... + if method_name == "derive_slot" + && self.std_ops_traits.contains("UniqueSlot") + && args.len() == 1 + { + if let Some(struct_info) = self.struct_types.get(type_name).cloned() { + let recv_ir = self.lower_expr(receiver)?; + let base_slot = self.lower_expr(&args[0])?; + let result = self.default_struct_derive_slot( + &recv_ir, + &base_slot, + &struct_info.fields, + ); + return Ok(result); + } } } @@ -257,6 +349,20 @@ impl AstToEgglog { let rhs = self.lower_expr(&args[0])?; return Ok(ast_helpers::bop(op, lhs, rhs)); } + + // Also check stateful methods for unknown receiver + { + let recv_ir = self.lower_expr(receiver)?; + let args_ir: Vec = args + .iter() + .map(|a| self.lower_expr(a)) + .collect::>()?; + if let Some(result) = + self.compiler_provided_stateful_method(method_name, Some(recv_ir), &args_ir) + { + return Ok(result); + } + } } // Fallback: treat as FunctionCall(FieldAccess(...), args) — lower normally @@ -338,6 +444,33 @@ impl AstToEgglog { let rhs = self.lower_expr(&args[1])?; return Ok(ast_helpers::bop(op, lhs, rhs)); } + + // Compiler-provided stateful methods (sload, sstore, derive_slot) + // For qualified calls: Sload::sload(slot) has no receiver (first arg is slot) + // Sstore::sstore(value, slot) has receiver as first arg + { + let args_ir: Vec = args + .iter() + .map(|a| self.lower_expr(a)) + .collect::>()?; + // For static methods like sload: no receiver, all args + if let Some(result) = + self.compiler_provided_stateful_method(method_name, None, &args_ir) + { + return Ok(result); + } + // For instance methods like sstore/derive_slot: first arg is receiver + if args_ir.len() >= 2 { + let recv = args_ir[0].clone(); + if let Some(result) = self.compiler_provided_stateful_method( + method_name, + Some(recv), + &args_ir[1..], + ) { + return Ok(result); + } + } + } } } @@ -496,7 +629,8 @@ impl AstToEgglog { return Some(ct.clone()); } // Fall back to primitive type name from EvmType - return Self::evm_type_to_name(&binding._ty); + let result = Self::evm_type_to_name(&binding._ty); + return result; } } None @@ -510,10 +644,108 @@ impl AstToEgglog { edge_ast::Lit::Int(_, None, _) => Some("u256".to_string()), _ => None, }, + // FieldAccess on self: `self.field` — look up the field binding + edge_ast::Expr::FieldAccess(obj, field, _) => { + if let edge_ast::Expr::Ident(ident) = obj.as_ref() { + if ident.name == "self" { + // Look up the field in scope + for scope in self.scopes.iter().rev() { + if let Some(binding) = scope.bindings.get(&field.name) { + if let Some(ref ct) = binding.composite_type { + return Some(ct.clone()); + } + return Self::evm_type_to_name(&binding._ty); + } + } + } + } + None + } + // ArrayIndex: base[index] — if base is a Map, the result type is the value type (V) + edge_ast::Expr::ArrayIndex(base, _, _, _) => { + let base_type = self.infer_receiver_type(base); + let base_args = self.infer_receiver_type_args(base); + if let Some(ref bt) = base_type { + if bt.starts_with("Map") && base_args.len() == 2 { + // V is the second type arg — use mangled name + return Some(Self::type_sig_mangle(&base_args[1])); + } + } + None + } _ => None, } } + /// Get the concrete type arguments for a receiver's generic composite type. + pub(crate) fn infer_receiver_type_args(&self, expr: &edge_ast::Expr) -> Vec { + match expr { + edge_ast::Expr::Ident(ident) => { + for scope in self.scopes.iter().rev() { + if let Some(binding) = scope.bindings.get(&ident.name) { + return binding.composite_type_args.clone(); + } + } + Vec::new() + } + edge_ast::Expr::FieldAccess(obj, field, _) => { + if let edge_ast::Expr::Ident(ident) = obj.as_ref() { + if ident.name == "self" { + for scope in self.scopes.iter().rev() { + if let Some(binding) = scope.bindings.get(&field.name) { + return binding.composite_type_args.clone(); + } + } + } + } + Vec::new() + } + // ArrayIndex: base[index] — if base is a Map, the result's type args come from V + edge_ast::Expr::ArrayIndex(base, _, _, _) => { + let base_args = self.infer_receiver_type_args(base); + if base_args.len() == 2 { + if let edge_ast::ty::TypeSig::Named(_, inner_args) = &base_args[1] { + return inner_args.clone(); + } + } + Vec::new() + } + _ => Vec::new(), + } + } + + /// Build a type parameter substitution map from a generic type's type params and concrete args. + /// E.g., for Map with args [addr, u256], returns {"K": "addr", "V": "u256"}. + /// For nested generics like Map>, V maps to "Map" (base name only). + fn build_type_param_subst( + &self, + type_name: &str, + type_args: &[edge_ast::ty::TypeSig], + ) -> HashMap { + if type_args.is_empty() { + return HashMap::new(); + } + // Try exact match first, then strip mangled suffix to find base template. + // E.g., "Map__CustomHash_CustomSStore" → try "Map" if exact lookup fails. + let template = self.generic_type_templates.get(type_name).or_else(|| { + let base = type_name.split("__").next().unwrap_or(type_name); + self.generic_type_templates.get(base) + }); + if let Some(template) = template { + template + .type_params + .iter() + .zip(type_args.iter()) + .map(|(param, arg)| { + let name = Self::type_sig_mangle(arg); + (param.name.name.clone(), name) + }) + .collect() + } else { + HashMap::new() + } + } + /// Convert an EvmType to a type name string (for primitives). fn evm_type_to_name(ty: &EvmType) -> Option { match ty { @@ -552,6 +784,7 @@ impl AstToEgglog { || type_name == "i256" || type_name == "bool" || type_name == "address" + || type_name == "b32" || type_name.starts_with("u") && type_name[1..].parse::().is_ok() || type_name.starts_with("i") @@ -571,6 +804,147 @@ impl AstToEgglog { } } + /// Compiler-provided complex trait methods for primitive types. + /// Unlike `compiler_provided_method` (simple binary ops), these produce + /// full IR expression trees with state threading. + /// + /// Returns `Some(ir_expr)` if the method was handled, `None` otherwise. + fn compiler_provided_stateful_method( + &mut self, + method_name: &str, + receiver_ir: Option, + args_ir: &[RcExpr], + ) -> Option { + use std::rc::Rc; + + match method_name { + // UniqueSlot::derive_slot(self, base_slot) → keccak256(key . base_slot) + "derive_slot" if self.std_ops_traits.contains("UniqueSlot") => { + let key = receiver_ir?; + let base_slot = args_ir.first()?; + let scratch = self.alloc_region(2); + // MSTORE(scratch, key) + let mstore_key = ast_helpers::mstore( + Rc::clone(&scratch), + key, + Rc::clone(&self.current_state), + ); + self.current_state = Rc::clone(&mstore_key); + // MSTORE(scratch+32, base_slot) + let slot_offset = ast_helpers::add( + Rc::clone(&scratch), + ast_helpers::const_int(32, self.current_ctx.clone()), + ); + let mstore_slot = ast_helpers::mstore( + slot_offset, + Rc::clone(base_slot), + Rc::clone(&self.current_state), + ); + self.current_state = Rc::clone(&mstore_slot); + // KECCAK256(scratch, 64, state) + let computed_slot = ast_helpers::keccak256( + scratch, + ast_helpers::const_int(64, self.current_ctx.clone()), + Rc::clone(&self.current_state), + ); + let side_effects = ast_helpers::concat(mstore_key, mstore_slot); + Some(ast_helpers::concat(side_effects, computed_slot)) + } + + // Sload::sload(slot) → SLOAD(slot, state) — static method (no receiver) + "sload" if self.std_ops_traits.contains("Sload") => { + let slot = if let Some(recv) = receiver_ir { + // Called as receiver.sload() — receiver is the slot + recv + } else { + // Called as Type::sload(slot) — first arg is the slot + args_ir.first()?.clone() + }; + Some(ast_helpers::sload(slot, Rc::clone(&self.current_state))) + } + + // Sstore::sstore(self, slot) → SSTORE(slot, value, state) + "sstore" if self.std_ops_traits.contains("Sstore") => { + let value = receiver_ir?; + let slot = args_ir.first()?; + let store = ast_helpers::sstore( + Rc::clone(slot), + value, + Rc::clone(&self.current_state), + ); + self.current_state = Rc::clone(&store); + Some(store) + } + + _ => None, + } + } + + /// Default `derive_slot` for struct types without an explicit `UniqueSlot` impl. + /// + /// Follows Solidity's nested mapping convention — each field is chained + /// through keccak256 as if it were a separate mapping level: + /// + /// ```text + /// slot = keccak256(field_0 . base_slot) + /// slot = keccak256(field_1 . slot) + /// slot = keccak256(field_2 . slot) + /// ... + /// ``` + fn default_struct_derive_slot( + &mut self, + receiver_ir: &RcExpr, + base_slot: &RcExpr, + fields: &[(String, EvmType)], + ) -> RcExpr { + let scratch = self.alloc_region(2); + let mut current_slot = Rc::clone(base_slot); + let mut side_effects = ast_helpers::empty( + EvmType::Base(EvmBaseType::UnitT), + self.current_ctx.clone(), + ); + + for (i, (_name, _ty)) in fields.iter().enumerate() { + // Load field value: MLOAD(receiver + i*32) + let field_offset = ast_helpers::add( + Rc::clone(receiver_ir), + ast_helpers::const_int((i * 32) as i64, self.current_ctx.clone()), + ); + let field_val = ast_helpers::mload(field_offset, Rc::clone(&self.current_state)); + + // MSTORE(scratch, field_value) + let mstore_field = ast_helpers::mstore( + Rc::clone(&scratch), + field_val, + Rc::clone(&self.current_state), + ); + self.current_state = Rc::clone(&mstore_field); + side_effects = ast_helpers::concat(side_effects, mstore_field); + + // MSTORE(scratch+32, current_slot) + let slot_offset = ast_helpers::add( + Rc::clone(&scratch), + ast_helpers::const_int(32, self.current_ctx.clone()), + ); + let mstore_slot = ast_helpers::mstore( + slot_offset, + current_slot, + Rc::clone(&self.current_state), + ); + self.current_state = Rc::clone(&mstore_slot); + side_effects = ast_helpers::concat(side_effects, mstore_slot); + + // slot = keccak256(scratch, 64) + current_slot = ast_helpers::keccak256( + Rc::clone(&scratch), + ast_helpers::const_int(64, self.current_ctx.clone()), + Rc::clone(&self.current_state), + ); + } + + ast_helpers::concat(side_effects, current_slot) + } + /// Infer the `EvmType` of an expression (best-effort, defaults to u256). pub(crate) fn infer_expr_type(&self, expr: &edge_ast::Expr) -> EvmType { match expr { @@ -656,12 +1030,55 @@ impl AstToEgglog { .collect::>()?; // Before pushing a new scope, look up composite info for args that are identifiers - // (needed for method calls where `self` refers to a struct variable) - let mut arg_composite: Vec)>> = Vec::new(); + // (needed for method calls where `self` refers to a struct variable or generic type) + tracing::trace!( + "inline_function_call: params={:?}, args={}", + params.iter().map(|(n, _)| n.as_str()).collect::>(), + args.len() + ); + let mut arg_composite: Vec, Vec)>> = Vec::new(); for arg in args { if let edge_ast::Expr::Ident(ident) = arg { let info = self.lookup_composite_info(&ident.name); - arg_composite.push(info.map(|(ct, cb)| (ct, Some(cb)))); + if let Some((ct, cb)) = info { + arg_composite.push(Some((ct, Some(cb), Vec::new()))); + } else { + // Check for composite_type without composite_base (e.g., Map type aliases) + let mut found = false; + for scope in self.scopes.iter().rev() { + if let Some(binding) = scope.bindings.get(&ident.name) { + if let Some(ref ct) = binding.composite_type { + arg_composite.push(Some((ct.clone(), None, binding.composite_type_args.clone()))); + found = true; + } + break; + } + } + if !found { + arg_composite.push(None); + } + } + } else if let edge_ast::Expr::ArrayIndex(base, _, _, _) = arg { + // For ArrayIndex args (e.g., map[key] as self parameter), + // infer the value type from the base Map's type args. + let base_type = self.infer_receiver_type(base); + let base_args = self.infer_receiver_type_args(base); + if let Some(ref bt) = base_type { + if bt.starts_with("Map") && base_args.len() == 2 { + let value_mangled = Self::type_sig_mangle(&base_args[1]); + // Extract inner type args if V is a generic type + let inner_args = if let edge_ast::ty::TypeSig::Named(_, inner) = &base_args[1] { + inner.clone() + } else { + Vec::new() + }; + arg_composite.push(Some((value_mangled, None, inner_args))); + } else { + arg_composite.push(None); + } + } else { + arg_composite.push(None); + } } else { arg_composite.push(None); } @@ -674,24 +1091,69 @@ impl AstToEgglog { .get(i) .cloned() .unwrap_or_else(|| ast_helpers::const_int(0, self.current_ctx.clone())); - let (mut composite_type, composite_base) = arg_composite + let (mut composite_type, mut composite_base, composite_type_args) = arg_composite .get(i) .and_then(|c| c.as_ref()) - .map(|(ct, cb)| (Some(ct.clone()), cb.clone())) - .unwrap_or((None, None)); + .map(|(ct, cb, ta)| (Some(ct.clone()), cb.clone(), ta.clone())) + .unwrap_or((None, None, Vec::new())); + + // If the parameter has a primitive type annotation, don't inherit + // composite_type from the argument — prevents Map type leaking through + // when Map.get passes `self` (Map) to derive_slot(base_slot: u256). + if matches!(param_ty, edge_ast::ty::TypeSig::Primitive(_)) && composite_type.is_some() { + // Only clear if the composite type doesn't match a known struct/union + // (the argument may be a struct disguised as u256 in the EVM) + if let Some(ref ct) = composite_type { + if !self.struct_types.contains_key(ct) && !self.union_types.contains_key(ct) { + composite_type = None; + } + } + } // If composite_type is still None, check if the param type sig names // a known struct/union type — this enables trait method dispatch on // generic parameters after monomorphization substitutes concrete types. + // Also resolve generic type parameters (K, V, etc.) through type_param_subst. if composite_type.is_none() { - if let edge_ast::ty::TypeSig::Named(ref name, _) = param_ty { - if self.struct_types.contains_key(&name.name) - || self.union_types.contains_key(&name.name) + if let edge_ast::ty::TypeSig::Named(ref name, ref type_args) = param_ty { + let resolved_name = self + .type_param_subst + .get(&name.name) + .cloned() + .unwrap_or_else(|| name.name.clone()); + if self.struct_types.contains_key(&resolved_name) + || self.union_types.contains_key(&resolved_name) { - composite_type = Some(name.name.clone()); + composite_type = Some(resolved_name); + } else if type_args.is_empty() { + // Check if resolved name is a generic type that was + // monomorphized (e.g., Result__u256) + let mangled = Self::type_sig_mangle(param_ty); + if self.struct_types.contains_key(&mangled) + || self.union_types.contains_key(&mangled) + { + composite_type = Some(mangled); + } } } } + + // If we inferred composite_type from the type sig but have no + // composite_base, set it to the param value — for struct types + // the value IS the memory base address. + if composite_type.is_some() && composite_base.is_none() { + if let Some(ref ct) = composite_type { + if self.struct_types.contains_key(ct) { + composite_base = Some(Rc::clone(&val)); + } + } + } + tracing::trace!( + " param={}, composite_type={:?}, has_base={}", + param_name, + composite_type, + composite_base.is_some() + ); let binding = VarBinding { value: val, location: DataLocation::Stack, @@ -700,6 +1162,7 @@ impl AstToEgglog { let_bind_name: None, composite_type, composite_base, + composite_type_args, }; self.scopes .last_mut() diff --git a/crates/ir/src/to_egglog/control_flow.rs b/crates/ir/src/to_egglog/control_flow.rs index 50ba542..dc861ef 100644 --- a/crates/ir/src/to_egglog/control_flow.rs +++ b/crates/ir/src/to_egglog/control_flow.rs @@ -66,6 +66,7 @@ impl AstToEgglog { let_bind_name: Some(var_name), composite_type: None, composite_base: None, + composite_type_args: Vec::new(), }, ); } @@ -176,6 +177,7 @@ impl AstToEgglog { let_bind_name: Some(var_name.clone()), composite_type: None, composite_base: None, + composite_type_args: Vec::new(), }; self.scopes .last_mut() diff --git a/crates/ir/src/to_egglog/expr.rs b/crates/ir/src/to_egglog/expr.rs index 9c308d6..a2bdbd7 100644 --- a/crates/ir/src/to_egglog/expr.rs +++ b/crates/ir/src/to_egglog/expr.rs @@ -132,6 +132,7 @@ impl AstToEgglog { let_bind_name: Some(var_name.clone()), composite_type, composite_base: None, + composite_type_args: Vec::new(), }; self.scopes .last_mut() @@ -194,6 +195,25 @@ impl AstToEgglog { } } self.last_composite_alloc = None; + + // Intercept ArrayIndex write for Index/Map dispatch: base[index] = val → base.set(index, val) + if let edge_ast::Expr::ArrayIndex(arr_base, arr_index, _, arr_span) = lhs { + if let Some(result) = self.try_lower_storage_array_write(arr_base, arr_index, &rhs_ir)? { + return Ok(result); + } + if let Some(result) = self.try_lower_array_element_write(arr_base, arr_index, &rhs_ir)? { + return Ok(result); + } + if self.std_ops_traits.contains("Index") { + return self.lower_method_call( + arr_base, + "set", + &[arr_index.as_ref().clone(), rhs.clone()], + arr_span, + ); + } + } + self.lower_assignment_with_composite(lhs, rhs_ir, rhs_composite.as_ref()) } @@ -212,6 +232,7 @@ impl AstToEgglog { let_bind_name: None, composite_type: None, composite_base: None, + composite_type_args: Vec::new(), }; self.scopes .last_mut() @@ -441,6 +462,25 @@ impl AstToEgglog { } } self.last_composite_alloc = None; + + // Intercept ArrayIndex write for Index/Map dispatch: base[index] = val → base.set(index, val) + if let edge_ast::Expr::ArrayIndex(arr_base, arr_index, _, arr_span) = lhs.as_ref() { + if let Some(result) = self.try_lower_storage_array_write(arr_base, arr_index, &rhs_ir)? { + return Ok(result); + } + if let Some(result) = self.try_lower_array_element_write(arr_base, arr_index, &rhs_ir)? { + return Ok(result); + } + if self.std_ops_traits.contains("Index") { + return self.lower_method_call( + arr_base, + "set", + &[arr_index.as_ref().clone(), rhs.as_ref().clone()], + arr_span, + ); + } + } + self.lower_assignment_with_composite(lhs, rhs_ir, rhs_composite.as_ref()) } @@ -500,8 +540,23 @@ impl AstToEgglog { } // Check if base is a memory-backed array/struct variable - self.try_lower_array_element_read(base, index)? - .map_or_else(|| self.lower_mapping_read(base, index), Ok) + if let Some(result) = self.try_lower_array_element_read(base, index)? { + return Ok(result); + } + + // Try Index trait dispatch: base[index] → base.index(index) + if self.std_ops_traits.contains("Index") { + return self.lower_method_call( + base, + "index", + &[index.as_ref().clone()], + _span, + ); + } + + Err(IrError::Unsupported( + "array index on non-array type; use Map.get(key) for mappings".to_owned(), + )) } edge_ast::Expr::Paren(inner, _span) => self.lower_expr(inner), @@ -680,6 +735,19 @@ impl AstToEgglog { // Search scopes from innermost to outermost for scope in self.scopes.iter().rev() { if let Some(binding) = scope.bindings.get(name) { + // Unit-typed storage fields return the slot constant directly. + // They have no data to SLOAD — their value IS the slot number. + // This enables types like Map = () to work as slot references. + // Note: () can lower as either Base(UnitT) or TupleT([]). + let is_unit = matches!(binding._ty, EvmType::Base(EvmBaseType::UnitT)) + || matches!(&binding._ty, EvmType::TupleT(v) if v.is_empty()); + if binding.storage_slot.is_some() && is_unit + { + return Ok(ast_helpers::const_int( + binding.storage_slot.unwrap_or(0) as i64, + self.current_ctx.clone(), + )); + } return match binding.location { DataLocation::Storage => { // Persistent storage variable: emit SLOAD @@ -821,8 +889,13 @@ impl AstToEgglog { if let Some(result) = self.try_lower_storage_array_write(base, index, &rhs_ir)? { return Ok(result); } - self.try_lower_array_element_write(base, index, &rhs_ir)? - .map_or_else(|| self.lower_mapping_write(base, index, rhs_ir), Ok) + if let Some(result) = self.try_lower_array_element_write(base, index, &rhs_ir)? { + return Ok(result); + } + // Index write dispatch is handled in the Assign branch above + Err(IrError::Unsupported( + "array index write on non-array type; use Map.set(key, val) for mappings".to_owned(), + )) } edge_ast::Expr::FieldAccess(obj, field, _span) => { // Storage-backed packed struct sub-field write: self.color.r = 5 @@ -1632,6 +1705,7 @@ impl AstToEgglog { let_bind_name: Some(var_name.clone()), composite_type: None, composite_base: None, + composite_type_args: Vec::new(), }; // Get the original name (without prefix) for scope lookup let orig_name = outputs diff --git a/crates/ir/src/to_egglog/function.rs b/crates/ir/src/to_egglog/function.rs index 48fd62a..edbc925 100644 --- a/crates/ir/src/to_egglog/function.rs +++ b/crates/ir/src/to_egglog/function.rs @@ -67,6 +67,7 @@ impl AstToEgglog { let_bind_name: None, composite_type: Some(format!("__array__{n}")), composite_base: Some(base_ir), + composite_type_args: Vec::new(), }; self.scopes .last_mut() @@ -74,6 +75,34 @@ impl AstToEgglog { .bindings .insert(ident.name.clone(), binding); calldata_offset += n * 32; + } else if let Some(struct_info) = self.resolve_struct_param_type(type_sig) { + // Struct parameter: allocate memory and copy fields from calldata + let n_fields = struct_info.fields.len(); + let base_ir = self.alloc_region(n_fields); + + // Copy each field from calldata to memory + let cd_off = + ast_helpers::const_int(calldata_offset as i64, self.current_ctx.clone()); + let size = ast_helpers::const_int((n_fields * 32) as i64, self.current_ctx.clone()); + let copy = ast_helpers::calldatacopy(Rc::clone(&base_ir), cd_off, size); + array_param_prefix = ast_helpers::concat(array_param_prefix, copy); + + let binding = VarBinding { + value: Rc::clone(&base_ir), + location: DataLocation::Stack, + storage_slot: None, + _ty: ty, + let_bind_name: None, + composite_type: Some(struct_info.name), + composite_base: Some(base_ir), + composite_type_args: Vec::new(), + }; + self.scopes + .last_mut() + .expect("scope stack empty") + .bindings + .insert(ident.name.clone(), binding); + calldata_offset += n_fields * 32; } else { // Scalar parameter: single 32-byte calldataload let raw_val = Rc::new(EvmExpr::Bop( @@ -105,6 +134,7 @@ impl AstToEgglog { let_bind_name: None, composite_type: None, composite_base: None, + composite_type_args: Vec::new(), }; self.scopes .last_mut() @@ -123,14 +153,38 @@ impl AstToEgglog { // Prepend array parameter loading before body let full_body = ast_helpers::concat(array_param_prefix, body_ir); - // Append a STOP (RETURN with 0 size) after the body. - // If the body already ends with RETURN, this is unreachable dead code. - let stop = ast_helpers::return_op( - ast_helpers::const_int(0, self.current_ctx.clone()), - ast_helpers::const_int(0, self.current_ctx.clone()), - Rc::clone(&self.current_state), - ); - Ok(ast_helpers::concat(full_body, stop)) + // If the function declares a return type and the body ends with a bare + // expression statement (trailing expression, Rust-style implicit return), + // wrap it with MSTORE + RETURN so the value is ABI-encoded in the output. + // Other endings (Return, Match, IfElse, etc.) handle their own returns. + let is_trailing_expr = body.stmts.last().is_some_and(|item| { + matches!(item, edge_ast::BlockItem::Stmt(s) if matches!(s.as_ref(), edge_ast::Stmt::Expr(..))) + || matches!(item, edge_ast::BlockItem::Expr(..)) + }); + if !fn_decl.returns.is_empty() && is_trailing_expr { + // Implicit return: body_ir's trailing value is the return value. + // Emit MSTORE(buf, value) + RETURN(buf, 32). + let ret_buf = self.alloc_region(1); + let size = ast_helpers::const_int(32, self.current_ctx.clone()); + let mstore_expr = ast_helpers::mstore( + Rc::clone(&ret_buf), + full_body, + Rc::clone(&self.current_state), + ); + self.current_state = Rc::clone(&mstore_expr); + let ret = + ast_helpers::return_op(ret_buf, size, Rc::clone(&self.current_state)); + Ok(ast_helpers::concat(mstore_expr, ret)) + } else { + // No return type, or body already has explicit return. + // Append RETURN(0, 0) as a fallthrough stop. + let stop = ast_helpers::return_op( + ast_helpers::const_int(0, self.current_ctx.clone()), + ast_helpers::const_int(0, self.current_ctx.clone()), + Rc::clone(&self.current_state), + ); + Ok(ast_helpers::concat(full_body, stop)) + } } /// Lower a standalone function. @@ -184,6 +238,7 @@ impl AstToEgglog { None }, composite_base: None, // dynamic base — resolved at element access + composite_type_args: Vec::new(), }; self.scopes .last_mut() @@ -323,19 +378,21 @@ impl AstToEgglog { // Lower all statements let mut stmts: Vec = Vec::new(); - for item in &block.stmts { + let last_idx = block.stmts.len().saturating_sub(1); + for (idx, item) in block.stmts.iter().enumerate() { let ir = match item { edge_ast::BlockItem::Stmt(stmt) => { - // Check for expression-statements with unused return values - if let edge_ast::Stmt::Expr(expr) = stmt.as_ref() { - self.check_unused_return_value(expr); + // Check for expression-statements with unused return values. + // Skip the last statement — it's the tail expression (block's + // return value) and its value IS consumed by the caller. + if idx != last_idx { + if let edge_ast::Stmt::Expr(expr) = stmt.as_ref() { + self.check_unused_return_value(expr); + } } self.lower_stmt(stmt)? } - edge_ast::BlockItem::Expr(expr) => { - self.check_unused_return_value(expr); - self.lower_expr(expr)? - } + edge_ast::BlockItem::Expr(expr) => self.lower_expr(expr)?, }; stmts.push(ir); } @@ -514,6 +571,7 @@ impl AstToEgglog { let_bind_name: None, composite_type: None, composite_base: None, + composite_type_args: Vec::new(), }; self.scopes .last_mut() @@ -548,4 +606,43 @@ impl AstToEgglog { self.lowered_functions.push(func_node); Ok(()) } + + /// Check if a parameter type sig resolves to a known struct type. + /// Returns (struct_name, field_count) if so. + pub(crate) fn resolve_struct_param_type( + &self, + type_sig: &edge_ast::ty::TypeSig, + ) -> Option { + let resolved = self.resolve_type_alias(type_sig); + let name = match resolved { + edge_ast::ty::TypeSig::Named(ident, _) => &ident.name, + _ => return None, + }; + + // Direct lookup + if let Some(info) = self.struct_types.get(name.as_str()) { + return Some(StructParamInfo { + name: name.clone(), + fields: info.fields.clone(), + }); + } + + // Try resolving through type_param_subst (for generic params like V → CustomSStore) + if let Some(resolved_name) = self.type_param_subst.get(name.as_str()) { + if let Some(info) = self.struct_types.get(resolved_name.as_str()) { + return Some(StructParamInfo { + name: resolved_name.clone(), + fields: info.fields.clone(), + }); + } + } + + None + } +} + +/// Info about a struct-typed function parameter. +pub(crate) struct StructParamInfo { + pub name: String, + pub fields: Vec<(String, EvmType)>, } diff --git a/crates/ir/src/to_egglog/mod.rs b/crates/ir/src/to_egglog/mod.rs index e25cb52..d547afd 100644 --- a/crates/ir/src/to_egglog/mod.rs +++ b/crates/ir/src/to_egglog/mod.rs @@ -14,7 +14,7 @@ mod pattern; mod storage; mod types; -use std::{collections::HashSet, rc::Rc}; +use std::{collections::{HashMap, HashSet}, rc::Rc}; use indexmap::IndexMap; @@ -93,6 +93,8 @@ pub(crate) struct VarBinding { pub composite_type: Option, /// For struct/array-typed variables: the memory base offset pub composite_base: Option, + /// For generic composite types: the concrete type arguments (e.g., [addr, u256] for Map) + pub composite_type_args: Vec, } /// Scope for variable resolution during lowering. @@ -136,6 +138,14 @@ pub(crate) struct GenericTypeTemplate { pub type_sig: edge_ast::ty::TypeSig, } +/// Stored impl block for a generic type, used during monomorphization. +#[derive(Debug, Clone)] +pub(crate) struct GenericImplBlock { + pub type_params: Vec, + pub trait_impl: Option, // trait name, or None for inherent impl + pub items: Vec, +} + /// Packed layout for a single field within a packed struct. #[derive(Debug, Clone)] pub(crate) struct PackedFieldLayout { @@ -276,6 +286,8 @@ pub struct AstToEgglog { pub(crate) inline_counter: usize, /// Prefix for variable names when inlining (empty at top level) pub(crate) inline_prefix: String, + /// Active type parameter substitutions (e.g., {"K": "addr", "V": "u256"} when inlining Map methods) + pub(crate) type_param_subst: HashMap, /// Union/enum type declarations: `type_name` -> `[(variant_name, has_data)]` /// Variant index is its position in the vector. pub(crate) union_types: IndexMap>, @@ -298,8 +310,10 @@ pub struct AstToEgglog { // ---- Generics & Traits ---- /// Generic type templates: name -> template info (type params + original `TypeSig`) pub(crate) generic_type_templates: IndexMap, + /// Generic impl blocks: base_type_name -> list of impl blocks (for monomorphization) + pub(crate) generic_impl_blocks: IndexMap>, /// Cache of monomorphized types: (`generic_name`, `concrete_types`) -> `mangled_name` - pub(crate) monomorphized_types: IndexMap<(String, Vec), String>, + pub(crate) monomorphized_types: IndexMap<(String, Vec), String>, /// Generic function templates: name -> `FreeFnInfo` (with `type_params`) pub(crate) generic_fn_templates: IndexMap, /// Cache of monomorphized function bodies: `mangled_name` -> `FreeFnInfo` @@ -349,6 +363,7 @@ impl AstToEgglog { inline_depth: 0, inline_counter: 0, inline_prefix: String::new(), + type_param_subst: HashMap::new(), union_types: IndexMap::new(), struct_types: IndexMap::new(), type_aliases: IndexMap::new(), @@ -357,6 +372,7 @@ impl AstToEgglog { last_composite_alloc: None, module_prefixes: HashSet::new(), generic_type_templates: IndexMap::new(), + generic_impl_blocks: IndexMap::new(), monomorphized_types: IndexMap::new(), generic_fn_templates: IndexMap::new(), monomorphized_fns: IndexMap::new(), @@ -379,6 +395,16 @@ impl AstToEgglog { crate::ast_helpers::mem_region(id, size_words as i64) } + /// Extract the type name and type args from a Named type sig, unwrapping Pointer wrappers. + /// Returns (base_name, type_args), e.g., ("Map", [addr, u256]) from `&s Map`. + fn extract_named_type(type_sig: &edge_ast::ty::TypeSig) -> Option<(String, Vec)> { + match type_sig { + edge_ast::ty::TypeSig::Named(name, args) => Some((name.name.clone(), args.clone())), + edge_ast::ty::TypeSig::Pointer(_, inner) => Self::extract_named_type(inner), + _ => None, + } + } + /// Lower an entire program. pub fn lower_program(&mut self, program: &edge_ast::Program) -> Result { let mut contracts = Vec::new(); @@ -420,7 +446,16 @@ impl AstToEgglog { "UnsafeAdd", "UnsafeSub", "UnsafeMul", + "UniqueSlot", + "Sload", + "Sstore", + "Index", ]; + // Storage/hashing traits are fundamental (auto-imported from globals). + // Always enable them so compiler-provided impls work without explicit `use`. + for name in ["UniqueSlot", "Sload", "Sstore", "Index"] { + self.std_ops_traits.insert(name.to_string()); + } for stmt in &program.stmts { if let edge_ast::Stmt::ModuleImport(import) = stmt { if import.root.name == "std" { @@ -456,6 +491,36 @@ impl AstToEgglog { } } + // Register compiler-provided trait impls for primitive types so that + // trait bound validation in monomorphize_type() passes for types like + // `Map` which requires `addr: UniqueSlot` and `u256: Sload & Sstore`. + { + let primitive_types = [ + "u256", "u248", "u240", "u232", "u224", "u216", "u208", "u200", + "u192", "u184", "u176", "u168", "u160", "u152", "u144", "u136", + "u128", "u120", "u112", "u104", "u96", "u88", "u80", "u72", + "u64", "u56", "u48", "u40", "u32", "u24", "u16", "u8", + "i256", "i248", "i240", "i232", "i224", "i216", "i208", "i200", + "i192", "i184", "i176", "i168", "i160", "i152", "i144", "i136", + "i128", "i120", "i112", "i104", "i96", "i88", "i80", "i72", + "i64", "i56", "i48", "i40", "i32", "i24", "i16", "i8", + "address", "bool", "b32", + ]; + let primitive_traits = ["UniqueSlot", "Sload", "Sstore"]; + for prim in &primitive_types { + for trait_name in &primitive_traits { + // Empty methods — compiler-provided dispatch handles actual codegen + self.trait_impls.insert( + (prim.to_string(), trait_name.to_string()), + TraitImplInfo { + methods: IndexMap::new(), + span: edge_types::span::Span::EOF, + }, + ); + } + } + } + // First pass: collect event declarations and free/comptime function bodies. // Free/comptime functions must be collected before const evaluation // because constants may call them (e.g. `const BASE_FEE = base_fee()`). @@ -528,6 +593,7 @@ impl AstToEgglog { let_bind_name: None, composite_type: None, composite_base: None, + composite_type_args: Vec::new(), }; self.scopes .last_mut() @@ -617,6 +683,22 @@ impl AstToEgglog { } edge_ast::Stmt::ImplBlock(impl_block) => { let type_name = impl_block.ty_name.name.clone(); + + // Store generic impl blocks for monomorphization + if !impl_block.type_params.is_empty() + || self.generic_type_templates.contains_key(&type_name) + { + let trait_name = impl_block.trait_impl.as_ref().map(|(n, _)| n.name.clone()); + self.generic_impl_blocks + .entry(type_name.clone()) + .or_default() + .push(GenericImplBlock { + type_params: impl_block.type_params.clone(), + trait_impl: trait_name, + items: impl_block.items.clone(), + }); + } + if let Some((ref trait_name, _)) = impl_block.trait_impl { // Trait impl — collect methods and validate against trait definition let mut methods = IndexMap::new(); @@ -854,7 +936,26 @@ impl AstToEgglog { self.storage_fields.push(field_ir); // Check if the field type resolves to a packed struct - let composite_type = self.resolve_storage_packed_struct_type(type_sig); + let mut composite_type = self.resolve_storage_packed_struct_type(type_sig); + + // For generic named types (e.g., Map), set composite_type to + // the monomorphized name so method dispatch finds concrete methods. + let mut composite_type_args = Vec::new(); + if composite_type.is_none() { + if let Some((name, args)) = Self::extract_named_type(type_sig) { + if !args.is_empty() { + // Use monomorphized name (e.g., "Map__address_u256") + if let Ok(mangled) = self.try_monomorphize_named_type(&name, &args, None) { + composite_type = mangled; + } else { + composite_type = Some(name); + } + } else { + composite_type = Some(name); + } + composite_type_args = args; + } + } // Register in scope with the correct location let binding = VarBinding { @@ -868,6 +969,7 @@ impl AstToEgglog { let_bind_name: None, composite_type, composite_base: None, + composite_type_args, }; self.scopes .last_mut() @@ -892,6 +994,7 @@ impl AstToEgglog { let_bind_name: None, composite_type: None, composite_base: None, + composite_type_args: Vec::new(), }; self.scopes .last_mut() diff --git a/crates/ir/src/to_egglog/pattern.rs b/crates/ir/src/to_egglog/pattern.rs index 9b5b4fb..9b14cac 100644 --- a/crates/ir/src/to_egglog/pattern.rs +++ b/crates/ir/src/to_egglog/pattern.rs @@ -126,6 +126,7 @@ impl AstToEgglog { let_bind_name: Some(var_name), composite_type: None, composite_base: None, + composite_type_args: Vec::new(), }, ); } @@ -204,6 +205,7 @@ impl AstToEgglog { let_bind_name: Some(var_name), composite_type: None, composite_base: None, + composite_type_args: Vec::new(), }, ); } diff --git a/crates/ir/src/to_egglog/storage.rs b/crates/ir/src/to_egglog/storage.rs index 9fa9164..c350fcf 100644 --- a/crates/ir/src/to_egglog/storage.rs +++ b/crates/ir/src/to_egglog/storage.rs @@ -1,4 +1,4 @@ -//! Storage and mapping lowering: emit, mapping slots, storage reads/writes. +//! Storage lowering: emit statements, storage field lookup. use std::rc::Rc; @@ -116,215 +116,6 @@ impl AstToEgglog { } } - /// Compute the storage slot for a mapping access. - /// - /// For `mapping[key]` at base slot `s`, Solidity uses: - /// `keccak256(abi.encode(key, s))` where key is left-padded to 32 bytes - /// at memory[0..32] and s is at memory[32..64]. - /// - /// Returns `(side_effects_expr, computed_slot_expr)` where `side_effects_expr` - /// is a Concat of MSTOREs that must be emitted before the slot is used. - pub(crate) fn compute_mapping_slot(&mut self, key: RcExpr, base_slot: i64) -> (RcExpr, RcExpr) { - let ctx = self.current_ctx.clone(); - // Allocate a 2-word scratch region for keccak input - let scratch = self.alloc_region(2); - // MSTORE(scratch, key) - let mstore_key = - ast_helpers::mstore(Rc::clone(&scratch), key, Rc::clone(&self.current_state)); - self.current_state = Rc::clone(&mstore_key); - // MSTORE(scratch+32, base_slot) - let slot_offset = - ast_helpers::add(Rc::clone(&scratch), ast_helpers::const_int(32, ctx.clone())); - let mstore_slot = ast_helpers::mstore( - slot_offset, - ast_helpers::const_int(base_slot, ctx.clone()), - Rc::clone(&self.current_state), - ); - self.current_state = Rc::clone(&mstore_slot); - // KECCAK256(scratch, 64, state) — state captures the memory contents - let computed_slot = ast_helpers::keccak256( - scratch, - ast_helpers::const_int(64, ctx), - Rc::clone(&self.current_state), - ); - let side_effects = ast_helpers::concat(mstore_key, mstore_slot); - (side_effects, computed_slot) - } - - /// Compute the storage slot for a nested mapping access. - /// - /// For `mapping[key1][key2]`, uses `keccak256(key2 . keccak256(key1 . base_slot))`. - /// - /// Uses memory[0..64] for the first level and memory[64..128] for the second - /// to avoid the second level's MSTORE overwriting the first level's data before - /// KECCAK256 reads it. - pub(crate) fn compute_nested_mapping_slot( - &mut self, - outer_key: RcExpr, - inner_key: RcExpr, - base_slot: i64, - ) -> (RcExpr, RcExpr) { - let ctx = self.current_ctx.clone(); - // Allocate two separate 2-word scratch regions so the second level's - // MSTORE doesn't overwrite the first level's data before KECCAK256. - let scratch1 = self.alloc_region(2); - let scratch2 = self.alloc_region(2); - // First level: keccak256(key1 . base_slot) at scratch1 - let mstore_key1 = ast_helpers::mstore( - Rc::clone(&scratch1), - outer_key, - Rc::clone(&self.current_state), - ); - self.current_state = Rc::clone(&mstore_key1); - let mstore_slot1 = ast_helpers::mstore( - ast_helpers::add( - Rc::clone(&scratch1), - ast_helpers::const_int(32, ctx.clone()), - ), - ast_helpers::const_int(base_slot, ctx.clone()), - Rc::clone(&self.current_state), - ); - self.current_state = Rc::clone(&mstore_slot1); - // inner_slot — KECCAK256(scratch1, 64, state) - let inner_slot = ast_helpers::keccak256( - scratch1, - ast_helpers::const_int(64, ctx.clone()), - Rc::clone(&self.current_state), - ); - // Second level: keccak256(key2 . inner_slot) at scratch2 - let mstore_key2 = ast_helpers::mstore( - Rc::clone(&scratch2), - inner_key, - Rc::clone(&self.current_state), - ); - self.current_state = Rc::clone(&mstore_key2); - let mstore_slot2 = ast_helpers::mstore( - ast_helpers::add( - Rc::clone(&scratch2), - ast_helpers::const_int(32, ctx.clone()), - ), - inner_slot, - Rc::clone(&self.current_state), - ); - self.current_state = Rc::clone(&mstore_slot2); - let computed_slot = ast_helpers::keccak256( - scratch2, - ast_helpers::const_int(64, ctx), - Rc::clone(&self.current_state), - ); - let side_effects = ast_helpers::concat( - ast_helpers::concat(mstore_key1, mstore_slot1), - ast_helpers::concat(mstore_key2, mstore_slot2), - ); - (side_effects, computed_slot) - } - - /// Lower a mapping read: `field[key]` or `field[key1][key2]`. - pub(crate) fn lower_mapping_read( - &mut self, - base: &edge_ast::Expr, - index: &edge_ast::Expr, - ) -> Result { - // Check for nested mapping: base is itself an ArrayIndex - if let edge_ast::Expr::ArrayIndex(outer_base, outer_index, _, _) = base { - // nested: outer_base[outer_index][index] - let field_name = match &**outer_base { - edge_ast::Expr::Ident(id) => &id.name, - _ => { - return Err(IrError::Unsupported( - "nested mapping on non-identifier".to_owned(), - )); - } - }; - let (base_slot, location) = self.find_storage_slot(field_name)?; - let outer_key = self.lower_expr(outer_index)?; - let inner_key = self.lower_expr(index)?; - let (side_effects, computed_slot) = - self.compute_nested_mapping_slot(outer_key, inner_key, base_slot as i64); - let load = match location { - DataLocation::Transient => { - ast_helpers::tload(computed_slot, Rc::clone(&self.current_state)) - } - _ => ast_helpers::sload(computed_slot, Rc::clone(&self.current_state)), - }; - return Ok(ast_helpers::concat(side_effects, load)); - } - - // Simple mapping: field[key] - let field_name = match base { - edge_ast::Expr::Ident(id) => &id.name, - _ => { - return Err(IrError::Unsupported( - "mapping on non-identifier base".to_owned(), - )); - } - }; - let (base_slot, location) = self.find_storage_slot(field_name)?; - let key = self.lower_expr(index)?; - let (side_effects, computed_slot) = self.compute_mapping_slot(key, base_slot as i64); - let load = match location { - DataLocation::Transient => { - ast_helpers::tload(computed_slot, Rc::clone(&self.current_state)) - } - _ => ast_helpers::sload(computed_slot, Rc::clone(&self.current_state)), - }; - Ok(ast_helpers::concat(side_effects, load)) - } - - /// Lower a mapping write: `field[key] = value` or `field[key1][key2] = value`. - pub(crate) fn lower_mapping_write( - &mut self, - base: &edge_ast::Expr, - index: &edge_ast::Expr, - value: RcExpr, - ) -> Result { - // Check for nested mapping - if let edge_ast::Expr::ArrayIndex(outer_base, outer_index, _, _) = base { - let field_name = match &**outer_base { - edge_ast::Expr::Ident(id) => &id.name, - _ => { - return Err(IrError::Unsupported( - "nested mapping on non-identifier".to_owned(), - )); - } - }; - let (base_slot, location) = self.find_storage_slot(field_name)?; - let outer_key = self.lower_expr(outer_index)?; - let inner_key = self.lower_expr(index)?; - let (side_effects, computed_slot) = - self.compute_nested_mapping_slot(outer_key, inner_key, base_slot as i64); - let store = match location { - DataLocation::Transient => { - ast_helpers::tstore(computed_slot, value, Rc::clone(&self.current_state)) - } - _ => ast_helpers::sstore(computed_slot, value, Rc::clone(&self.current_state)), - }; - self.current_state = Rc::clone(&store); - return Ok(ast_helpers::concat(side_effects, store)); - } - - // Simple mapping write - let field_name = match base { - edge_ast::Expr::Ident(id) => &id.name, - _ => { - return Err(IrError::Unsupported( - "mapping on non-identifier base".to_owned(), - )); - } - }; - let (base_slot, location) = self.find_storage_slot(field_name)?; - let key = self.lower_expr(index)?; - let (side_effects, computed_slot) = self.compute_mapping_slot(key, base_slot as i64); - let store = match location { - DataLocation::Transient => { - ast_helpers::tstore(computed_slot, value, Rc::clone(&self.current_state)) - } - _ => ast_helpers::sstore(computed_slot, value, Rc::clone(&self.current_state)), - }; - self.current_state = Rc::clone(&store); - Ok(ast_helpers::concat(side_effects, store)) - } - /// Find the storage slot index and data location for a named field. pub(crate) fn find_storage_slot(&self, name: &str) -> Result<(usize, DataLocation), IrError> { for scope in self.scopes.iter().rev() { diff --git a/crates/ir/src/to_egglog/types.rs b/crates/ir/src/to_egglog/types.rs index 9b847fd..c883988 100644 --- a/crates/ir/src/to_egglog/types.rs +++ b/crates/ir/src/to_egglog/types.rs @@ -2,6 +2,7 @@ use std::collections::{HashMap, HashSet}; +use indexmap::IndexMap; use super::{AstToEgglog, StructTypeInfo}; use crate::{ schema::{DataLocation, EvmBaseType, EvmType}, @@ -285,6 +286,119 @@ impl AstToEgglog { } } + /// Substitute type parameters in a code block (AST-level). + /// Replaces type param names in Path expressions (e.g., V::sload → u256::sload). + /// For generic types like Map, uses the mangled name (Map__address_u256) + /// so that qualified calls resolve to monomorphized trait impls. + fn substitute_code_block( + block: &edge_ast::CodeBlock, + subst: &HashMap, + ) -> edge_ast::CodeBlock { + // Build a string→string map for path substitution using mangled names + let name_subst: HashMap<&str, String> = subst.iter().map(|(k, v)| { + (k.as_str(), Self::type_sig_mangle(v)) + }).collect(); + + edge_ast::CodeBlock { + stmts: block.stmts.iter().map(|item| { + Self::substitute_block_item(item, &name_subst) + }).collect(), + span: block.span.clone(), + } + } + + fn substitute_block_item( + item: &edge_ast::stmt::BlockItem, + subst: &HashMap<&str, String>, + ) -> edge_ast::stmt::BlockItem { + match item { + edge_ast::stmt::BlockItem::Stmt(stmt) => { + edge_ast::stmt::BlockItem::Stmt(Box::new(Self::substitute_stmt(stmt, subst))) + } + edge_ast::stmt::BlockItem::Expr(expr) => { + edge_ast::stmt::BlockItem::Expr(Self::substitute_expr(expr, subst)) + } + } + } + + fn substitute_stmt( + stmt: &edge_ast::Stmt, + subst: &HashMap<&str, String>, + ) -> edge_ast::Stmt { + match stmt { + edge_ast::Stmt::VarDecl(ident, ty, init, span) => { + edge_ast::Stmt::VarDecl( + ident.clone(), + ty.clone(), + init.as_ref().map(|e| Box::new(Self::substitute_expr(e, subst))), + span.clone(), + ) + } + edge_ast::Stmt::VarAssign(lhs, rhs, span) => { + edge_ast::Stmt::VarAssign( + Self::substitute_expr(lhs, subst), + Self::substitute_expr(rhs, subst), + span.clone(), + ) + } + edge_ast::Stmt::Return(Some(expr), span) => { + edge_ast::Stmt::Return(Some(Self::substitute_expr(expr, subst)), span.clone()) + } + edge_ast::Stmt::Expr(expr) => { + edge_ast::Stmt::Expr(Self::substitute_expr(expr, subst)) + } + other => other.clone(), + } + } + + fn substitute_expr( + expr: &edge_ast::Expr, + subst: &HashMap<&str, String>, + ) -> edge_ast::Expr { + match expr { + edge_ast::Expr::Path(components, span) => { + let new_components: Vec = components.iter().map(|c| { + if let Some(replacement) = subst.get(c.name.as_str()) { + edge_ast::Ident { name: replacement.clone(), span: c.span.clone() } + } else { + c.clone() + } + }).collect(); + edge_ast::Expr::Path(new_components, span.clone()) + } + edge_ast::Expr::FunctionCall(callee, args, turbofish, span) => { + edge_ast::Expr::FunctionCall( + Box::new(Self::substitute_expr(callee, subst)), + args.iter().map(|a| Self::substitute_expr(a, subst)).collect(), + turbofish.clone(), + span.clone(), + ) + } + edge_ast::Expr::FieldAccess(obj, field, span) => { + edge_ast::Expr::FieldAccess( + Box::new(Self::substitute_expr(obj, subst)), + field.clone(), + span.clone(), + ) + } + edge_ast::Expr::Binary(lhs, op, rhs, span) => { + edge_ast::Expr::Binary( + Box::new(Self::substitute_expr(lhs, subst)), + op.clone(), + Box::new(Self::substitute_expr(rhs, subst)), + span.clone(), + ) + } + edge_ast::Expr::Paren(inner, span) => { + edge_ast::Expr::Paren( + Box::new(Self::substitute_expr(inner, subst)), + span.clone(), + ) + } + _ => expr.clone(), + } + } + /// Try to monomorphize a generic union from a variant constructor call. /// /// Given `Result::Ok(42)` where `Result = Ok(T) | Err(u256)`: @@ -354,12 +468,13 @@ impl AstToEgglog { type_args: &[edge_ast::ty::TypeSig], span: Option<&edge_types::span::Span>, ) -> Result { - // Lower type args to EvmType for caching - let concrete_types: Vec = - type_args.iter().map(|t| self.lower_type_sig(t)).collect(); + // Use mangled type names for caching — EvmType loses source-level + // distinctions (e.g., CustomHash and u256 both lower to UIntT(256)). + let cache_key_types: Vec = + type_args.iter().map(Self::type_sig_mangle).collect(); // Check cache - let cache_key = (generic_name.to_string(), concrete_types); + let cache_key = (generic_name.to_string(), cache_key_types); if let Some(mangled) = self.monomorphized_types.get(&cache_key) { return Ok(mangled.clone()); } @@ -408,9 +523,27 @@ impl AstToEgglog { for (tp, arg) in template.type_params.iter().zip(type_args.iter()) { if !tp.constraints.is_empty() { let concrete_name = Self::type_sig_display(arg); + // For generic type args (e.g., Map), also try the mangled name + // since monomorphized impls are registered under the mangled name. + let mangled_name = if let edge_ast::ty::TypeSig::Named(name, inner_args) = arg { + if !inner_args.is_empty() { + // Ensure the inner type is monomorphized first + match self.try_monomorphize_named_type(&name.name, inner_args, span) { + Ok(Some(m)) => Some(m), + _ => None, + } + } else { + None + } + } else { + None + }; for constraint in &tp.constraints { let key = (concrete_name.clone(), constraint.name.clone()); - if !self.trait_impls.contains_key(&key) { + let mangled_key = mangled_name.as_ref().map(|m| (m.clone(), constraint.name.clone())); + let satisfied = self.trait_impls.contains_key(&key) + || mangled_key.as_ref().map_or(false, |k| self.trait_impls.contains_key(k)); + if !satisfied { let mut diag = edge_diagnostics::Diagnostic::error(format!( "the trait bound `{}: {}` is not satisfied", concrete_name, constraint.name, @@ -442,9 +575,8 @@ impl AstToEgglog { .map(|(param, arg)| (param.name.name.clone(), arg.clone())) .collect(); - // Use source-level type names for mangling to distinguish struct types - // that lower to the same EVM representation. - let type_name_strs: Vec = type_args.iter().map(Self::type_sig_display).collect(); + // Use mangled type names for identifier-safe names (no angle brackets). + let type_name_strs: Vec = type_args.iter().map(Self::type_sig_mangle).collect(); let mangled = format!("{generic_name}__{}", type_name_strs.join("_")); // Substitute and register @@ -480,6 +612,80 @@ impl AstToEgglog { } } + // Monomorphize impl blocks for this generic type + if let Some(impl_blocks) = self.generic_impl_blocks.get(generic_name).cloned() { + for gib in &impl_blocks { + // Build substitution from the generic impl's type params to concrete args + let impl_subst: HashMap = if gib.type_params.is_empty() { + // Use the type template's params (e.g., `impl Map` where K,V from the type) + subst.clone() + } else { + gib.type_params.iter() + .zip(type_args.iter()) + .map(|(param, arg)| (param.name.name.clone(), arg.clone())) + .collect() + }; + + // Substitute type params in method bodies and register under mangled name + let concrete_methods: Vec = gib.items.iter().map(|item| { + match item { + edge_ast::item::ImplItem::FnAssign(fn_decl, body) => { + let new_params: Vec<(edge_ast::Ident, edge_ast::ty::TypeSig)> = fn_decl.params.iter().map(|(id, ty)| { + (id.clone(), Self::substitute_type_params(ty, &impl_subst)) + }).collect(); + let new_returns: Vec = fn_decl.returns.iter().map(|ty| { + Self::substitute_type_params(ty, &impl_subst) + }).collect(); + let new_fn_decl = edge_ast::item::FnDecl { + name: fn_decl.name.clone(), + params: new_params, + returns: new_returns, + type_params: Vec::new(), // concrete, no type params + is_pub: fn_decl.is_pub, + is_ext: fn_decl.is_ext, + is_mut: fn_decl.is_mut, + span: fn_decl.span.clone(), + }; + // Substitute type params in body expressions + let new_body = Self::substitute_code_block(body, &impl_subst); + edge_ast::item::ImplItem::FnAssign(new_fn_decl, new_body) + } + other => other.clone(), + } + }).collect(); + + if let Some(ref trait_name) = gib.trait_impl { + // Trait impl: register under mangled type name + let mut methods = IndexMap::new(); + for item in &concrete_methods { + if let edge_ast::item::ImplItem::FnAssign(fn_decl, body) = item { + methods.insert(fn_decl.name.name.clone(), (fn_decl.clone(), body.clone())); + } + } + self.trait_impls.insert( + (mangled.clone(), trait_name.clone()), + super::TraitImplInfo { + methods, + span: edge_types::span::Span::EOF, + }, + ); + } else { + // Inherent impl: register methods under mangled type name + let methods: Vec = concrete_methods.iter().filter_map(|item| { + if let edge_ast::item::ImplItem::FnAssign(fn_decl, body) = item { + Some(super::InherentMethod { + fn_decl: fn_decl.clone(), + body: body.clone(), + }) + } else { + None + } + }).collect(); + self.inherent_methods.entry(mangled.clone()).or_default().extend(methods); + } + } + } + self.monomorphized_types.insert(cache_key, mangled.clone()); Ok(mangled) } @@ -574,10 +780,49 @@ impl AstToEgglog { Ok(()) } + /// Mangle a `TypeSig` into an identifier-safe name for use as mangled type names. + /// E.g., `Map` → `Map__address_u256`, nested types recursively mangled. + pub(crate) fn type_sig_mangle(ty: &edge_ast::ty::TypeSig) -> String { + match ty { + edge_ast::ty::TypeSig::Primitive(p) => { + use edge_ast::ty::PrimitiveType; + match p { + PrimitiveType::UInt(n) => format!("u{n}"), + PrimitiveType::Int(n) => format!("i{n}"), + PrimitiveType::FixedBytes(n) => format!("b{n}"), + PrimitiveType::Address => "address".to_string(), + PrimitiveType::Bool => "bool".to_string(), + PrimitiveType::Bit => "bit".to_string(), + } + } + edge_ast::ty::TypeSig::Named(ident, args) => { + if args.is_empty() { + ident.name.clone() + } else { + let arg_strs: Vec = args.iter() + .map(Self::type_sig_mangle) + .collect(); + format!("{}__{}", ident.name, arg_strs.join("_")) + } + } + _ => "unknown".to_string(), + } + } + /// Simple display for a `TypeSig` (for error messages). pub(crate) fn type_sig_display(ty: &edge_ast::ty::TypeSig) -> String { match ty { - edge_ast::ty::TypeSig::Primitive(p) => format!("{p:?}").to_lowercase(), + edge_ast::ty::TypeSig::Primitive(p) => { + use edge_ast::ty::PrimitiveType; + match p { + PrimitiveType::UInt(n) => format!("u{n}"), + PrimitiveType::Int(n) => format!("i{n}"), + PrimitiveType::FixedBytes(n) => format!("b{n}"), + PrimitiveType::Address => "address".to_string(), + PrimitiveType::Bool => "bool".to_string(), + PrimitiveType::Bit => "bit".to_string(), + } + } edge_ast::ty::TypeSig::Named(ident, args) => { if args.is_empty() { ident.name.clone() diff --git a/crates/parser/src/parser.rs b/crates/parser/src/parser.rs index 175e86f..78248f4 100644 --- a/crates/parser/src/parser.rs +++ b/crates/parser/src/parser.rs @@ -238,7 +238,39 @@ impl Parser { if token.kind == kind { Ok(self.advance()) } else { - Err(ParseError::unexpected(&token.kind, &kind, token.span)) + // For missing delimiters (;, ), ], }), point at the end of the + // previous token — that's where the delimiter was expected. + let span = if matches!( + kind, + TokenKind::Semicolon + | TokenKind::CloseParen + | TokenKind::CloseBracket + | TokenKind::CloseBrace + ) { + self.prev_token_end_span().unwrap_or(token.span) + } else { + token.span + }; + Err(ParseError::unexpected(&token.kind, &kind, span)) + } + } + + /// Check if the token after the current one is `::` (without advancing). + fn lookahead_double_colon(&self) -> bool { + self.cursor + 1 < self.tokens.len() && self.tokens[self.cursor + 1].kind == TokenKind::DoubleColon + } + + /// Get a zero-width span at the end of the previous token. + fn prev_token_end_span(&self) -> Option { + if self.cursor > 0 { + let prev = &self.tokens[self.cursor - 1]; + Some(Span { + start: prev.span.end, + end: prev.span.end, + file: prev.span.file.clone(), + }) + } else { + None } } @@ -1718,6 +1750,88 @@ impl Parser { let lit = Lit::Str(s, token.span); Ok(Expr::Literal(Box::new(lit))) } + // Primitive type used as a path root: u256::sload(...), address::default(), etc. + TokenKind::DataType(ref dt) if self.lookahead_double_colon() => { + let name = match dt { + edge_types::tokens::DataType::Primitive(pt) => { + let ast_pt = self.convert_primitive_type(pt.clone()); + ast_pt.to_string() + } + edge_types::tokens::DataType::Unknown => { + return Err(ParseError::InvalidExpr { + message: "Unknown data type".to_string(), + span: self.peek().span.clone(), + }); + } + }; + let token = self.advance(); + let ident = Ident { + name, + span: token.span.clone(), + }; + + // Parse :: path (same as Ident path handling below) + let mut path_segments = vec![ident]; + let mut turbofish_type_args: Vec = vec![]; + while self.check(&TokenKind::DoubleColon) { + self.advance(); + if self.check(&TokenKind::Operator(Operator::Comparison( + ComparisonOperator::LessThan, + ))) { + turbofish_type_args = self.parse_turbofish_type_args()?; + break; + } + if let TokenKind::Ident(next_name) = self.peek().kind.clone() { + let next_token = self.advance(); + path_segments.push(Ident { + name: next_name, + span: next_token.span, + }); + } else { + return Err(ParseError::InvalidExpr { + message: "Expected identifier after ::".to_string(), + span: self.peek().span.clone(), + }); + } + } + + self.skip_whitespace_and_comments(); + if self.check(&TokenKind::OpenParen) { + self.advance(); + let mut args = Vec::new(); + while !self.check(&TokenKind::CloseParen) && !self.is_at_end() { + self.skip_whitespace_and_comments(); + if self.check(&TokenKind::CloseParen) { + break; + } + args.push(self.parse_expr()?); + self.skip_whitespace_and_comments(); + if !self.check(&TokenKind::CloseParen) { + self.expect(TokenKind::Comma)?; + } + } + let end = self.expect(TokenKind::CloseParen)?; + let span = Span { + start: token.span.start, + end: end.span.end, + file: token.span.file, + }; + Ok(Expr::FunctionCall( + Box::new(Expr::Path(path_segments, span.clone())), + args, + turbofish_type_args, + span, + )) + } else { + let end_span = path_segments.last().unwrap().span.clone(); + let span = Span { + start: token.span.start, + end: end_span.end, + file: token.span.file, + }; + Ok(Expr::Path(path_segments, span)) + } + } TokenKind::Ident(name) => { let token = self.advance(); let ident = Ident { diff --git a/examples/erc20.edge b/examples/erc20.edge index fead975..3016c97 100644 --- a/examples/erc20.edge +++ b/examples/erc20.edge @@ -37,10 +37,10 @@ contract ERC20 { let total_supply: &s u256; // Balances mapping: account -> amount - let balances: &s map; + let balances: &s Map; // Allowances mapping: owner -> spender -> amount - let allowances: &s map>; + let allowances: &s Map>; // Metadata functions pub fn totalSupply() -> (u256) { diff --git a/examples/tests/stress_loops.edge b/examples/tests/stress_loops.edge index 55a3815..9e65673 100644 --- a/examples/tests/stress_loops.edge +++ b/examples/tests/stress_loops.edge @@ -11,7 +11,7 @@ abi IStressLoops { } contract StressLoops { - let values: &s u256; + let values: &s Map; let count: &s u256; // Simple accumulator loop: sum 1..n. diff --git a/examples/tests/stress_storage.edge b/examples/tests/stress_storage.edge index a2b92da..8c2027e 100644 --- a/examples/tests/stress_storage.edge +++ b/examples/tests/stress_storage.edge @@ -19,8 +19,8 @@ abi IStressStorage { contract StressStorage { let total: &s u256; - let balances: &s u256; - let nonces: &s u256; + let balances: &s Map; + let nonces: &s Map; // Reads @caller, reads balance, writes balance, writes total, emits event. // Tests: interleaved SLOAD, SSTORE, MSTORE for keccak, and LetBind variables. diff --git a/examples/tests/test_erc20.edge b/examples/tests/test_erc20.edge index 7ee4c64..cf0e6e3 100644 --- a/examples/tests/test_erc20.edge +++ b/examples/tests/test_erc20.edge @@ -17,8 +17,8 @@ abi IERC20Test { contract ERC20Test { let total_supply: &s u256; - let balances: &s u256; - let allowances: &s u256; + let balances: &s Map; + let allowances: &s Map>; pub fn totalSupply() -> (u256) { return total_supply; diff --git a/examples/tests/test_map_std.edge b/examples/tests/test_map_std.edge new file mode 100644 index 0000000..ce660e0 --- /dev/null +++ b/examples/tests/test_map_std.edge @@ -0,0 +1,120 @@ + +use std::ops::UniqueSlot; +use std::ops::Sstore; +use std::ops::Sload; + +type CustomSStore = { + ignored: u256, + packed_a: u128, + packed_b: u128 +}; + +type CustomHash = { + a: u128, + b: u128 +}; + +// Struct key with NO UniqueSlot impl — uses default keccak-chained derive_slot +type DefaultKey = { + x: u256, + y: u256 +}; + +impl CustomHash: UniqueSlot { + fn derive_slot(self, base_slot: u256) -> u256 { + let packed_combo: u256 = (self.a << 128) | self.b; + base_slot + packed_combo + } +} + +impl CustomSStore: Sstore { + fn sstore(self, base_slot: u256) { + let packed_combo: u256 = (self.packed_a << 128) | self.packed_b; + packed_combo.sstore(base_slot); + } +} + +impl CustomSStore: Sload { + fn sload(base_slot: u256) -> Self { + let packed_combo = u256::sload(base_slot); + let a = packed_combo >> 128; + let b = packed_combo & ((1 << 128) - 1); + CustomSStore { ignored: 0, packed_a: a, packed_b: b } + } +} + +contract TestMappings { + let basic_map: &s Map; + let custom: &s CustomSStore; + let custom_sstore_map: &s Map; + let double_custom_sstore_map: &s Map; + let default_key_map: &s Map; + + // Getters + pub fn get_custom() -> CustomSStore { + custom + } + + pub fn get_basic(key: u256) -> u256 { + basic_map.get(key) + } + + pub fn get_basic_by_indexable(key: u256) -> u256 { + basic_map[key] + } + + pub fn get_custom(key: u256) -> u256 { + let val: CustomSStore = custom_sstore_map.get(key); + (val.packed_a << 128) | val.packed_b + } + + pub fn get_custom_by_indexable(key: u256) -> u256 { + let val: CustomSStore = custom_sstore_map[key]; + (val.packed_a << 128) | val.packed_b + } + + // Setters + pub mut fn set_custom(a: u128, b: u128) { + custom = CustomSStore { ignored: 10000, packed_a: a, packed_b: b } + } + + pub mut fn set_basic(key: u256, val: u256) -> u256 { + basic_map.set(key, val); + } + + pub mut fn set_basic_by_indexable(key: u256, val: u256) -> u256 { + basic_map[key] = val; + } + + pub mut fn set_custom(key: u256, val: CustomSStore) { + custom_sstore_map.set(key, val); + } + + pub mut fn set_custom_by_indexable(key: u256, val: CustomSStore) { + custom_sstore_map[key] = val; + } + + // Double custom: CustomHash key + CustomSStore value + pub fn get_double_custom(a: u128, b: u128) -> u256 { + let key = CustomHash { a: a, b: b }; + let val: CustomSStore = double_custom_sstore_map.get(key); + (val.packed_a << 128) | val.packed_b + } + + pub mut fn set_double_custom(a: u128, b: u128, val_a: u128, val_b: u128) { + let key = CustomHash { a: a, b: b }; + let val = CustomSStore { ignored: 0, packed_a: val_a, packed_b: val_b }; + double_custom_sstore_map.set(key, val); + } + + // Default derive_slot (no UniqueSlot impl on DefaultKey) + pub fn get_default_key(x: u256, y: u256) -> u256 { + let key = DefaultKey { x: x, y: y }; + default_key_map.get(key) + } + + pub mut fn set_default_key(x: u256, y: u256, val: u256) { + let key = DefaultKey { x: x, y: y }; + default_key_map.set(key, val); + } +} diff --git a/examples/tests/test_mappings.edge b/examples/tests/test_mappings.edge index c6da0fc..5738a92 100644 --- a/examples/tests/test_mappings.edge +++ b/examples/tests/test_mappings.edge @@ -1,9 +1,9 @@ // test_mappings.edge — Execution tests for simple and nested mappings contract TestMappings { - let balances: &s u256; - let allowances: &s u256; - let counters: &s u256; + let balances: &s Map; + let allowances: &s Map>; + let counters: &s Map; // Simple mapping: set and get pub fn map_set(key: addr, value: u256) { diff --git a/examples/tests/test_storage_heavy.edge b/examples/tests/test_storage_heavy.edge index 886e63a..0cf0a28 100644 --- a/examples/tests/test_storage_heavy.edge +++ b/examples/tests/test_storage_heavy.edge @@ -21,8 +21,8 @@ contract TestStorageHeavy { let field_c: &s u256; let field_d: &s u256; let field_e: &s u256; - let balances: &s u256; - let allowances: &s u256; + let balances: &s Map; + let allowances: &s Map>; pub fn set_all(a: u256, b: u256, c: u256, d: u256, e: u256) { field_a = a; diff --git a/examples/tokens/erc20.edge b/examples/tokens/erc20.edge index 3f43301..dd3f807 100644 --- a/examples/tokens/erc20.edge +++ b/examples/tokens/erc20.edge @@ -48,10 +48,10 @@ contract Airdrop { // ── State ───────────────────────────────────────────────────────────────── // Amount of tokens each address is entitled to claim. - let allocations: &s map; + let allocations: &s Map; // Tracks whether an address has already claimed their allocation. - let claimed: &s map; + let claimed: &s Map; // Total tokens distributed so far. let total_distributed: &s u256; diff --git a/examples/tokens/erc721.edge b/examples/tokens/erc721.edge index aef0d5a..92d6059 100644 --- a/examples/tokens/erc721.edge +++ b/examples/tokens/erc721.edge @@ -57,7 +57,7 @@ contract ArtCollection { let mint_open: &s bool; // Per-token URIs for metadata (tokenId -> URI). - let token_uris: &s map; + let token_uris: &s Map; // Base URI prepended to all token metadata paths. let base_uri: &s b32; diff --git a/std/access/roles.edge b/std/access/roles.edge index ccccbcf..1b178f5 100644 --- a/std/access/roles.edge +++ b/std/access/roles.edge @@ -1,7 +1,7 @@ // roles.edge — Multi-role authority with role-based access control // // What this demonstrates: -// - Nested maps: map> +// - Nested maps: Map> // - const role identifiers (b32 type) // - hasRole/grantRole/revokeRole pattern // - Events with indexed fields @@ -43,10 +43,10 @@ abi IAccessControl { contract AccessControl { // Nested mapping: role -> account -> has role. - let roles: &s map>; + let roles: &s Map>; // Mapping from role to its admin role. - let role_admin: &s map; + let role_admin: &s Map; // Check if an account has a specific role. pub fn hasRole(role: b32, account: addr) -> (bool) { diff --git a/std/finance/amm.edge b/std/finance/amm.edge index e16eb14..38caeff 100644 --- a/std/finance/amm.edge +++ b/std/finance/amm.edge @@ -6,7 +6,7 @@ // - Events: Swap, AddLiquidity, RemoveLiquidity // - abi interface definition (IAMM) // - @caller() builtin -// - map storage mapping +// - Map storage mapping // - Complex arithmetic expressions // - Multiple pub fn functions // @@ -50,7 +50,7 @@ contract AMM { // LP token state. let total_supply: &s u256; - let lp_balances: &s map; + let lp_balances: &s Map; // Token addresses. let token0: &s addr; diff --git a/std/finance/multisig.edge b/std/finance/multisig.edge index da22d39..bf8248c 100644 --- a/std/finance/multisig.edge +++ b/std/finance/multisig.edge @@ -45,28 +45,28 @@ contract Multisig { let owner_count: &s u256; // Whether an address is an owner. - let is_owner: &s map; + let is_owner: &s Map; // Proposal counter (next proposal ID). let proposal_count: &s u256; // Proposal targets. - let proposal_targets: &s map; + let proposal_targets: &s Map; // Proposal values. - let proposal_values: &s map; + let proposal_values: &s Map; // Proposal data. - let proposal_data: &s map; + let proposal_data: &s Map; // Proposal states: 0=Pending, 1=Approved, 2=Executed, 3=Cancelled. - let proposal_states: &s map; + let proposal_states: &s Map; // Number of confirmations per proposal. - let confirmation_count: &s map; + let confirmation_count: &s Map; // Whether an owner has confirmed a proposal. - let confirmations: &s map>; + let confirmations: &s Map>; // Create a new proposal. Only owners can propose. pub fn propose(target: addr, value: u256, data: b32) -> (u256) { diff --git a/std/finance/staking.edge b/std/finance/staking.edge index 94564fb..127aad8 100644 --- a/std/finance/staking.edge +++ b/std/finance/staking.edge @@ -4,7 +4,7 @@ // - Complex &s state: multiple maps, u256 arithmetic // - Staked/Withdrawn/RewardPaid events // - while loops for time-based reward calculations -// - map storage mappings +// - Map storage mappings // - @caller() builtin // - Internal helper functions for reward accumulation // - Arithmetic: multiplication, division, addition, subtraction @@ -47,13 +47,13 @@ contract Staking { let total_staked: &s u256; // Per-user staked balances. - let staked_balances: &s map; + let staked_balances: &s Map; // Per-user accumulated rewards. - let rewards: &s map; + let rewards: &s Map; // Per-user reward per token paid (for pro-rata calculation). - let user_reward_per_token_paid: &s map; + let user_reward_per_token_paid: &s Map; // Global reward per token stored. let reward_per_token_stored: &s u256; diff --git a/std/globals/map.edge b/std/globals/map.edge new file mode 100644 index 0000000..6a0e847 --- /dev/null +++ b/std/globals/map.edge @@ -0,0 +1,87 @@ +// map.edge — Generic storage mapping type +// +// Map is a zero-storage type — at runtime it's just a u256 (the base slot). +// K and V are phantom type parameters that guide dispatch via monomorphization. +// +// Key insight for nested maps (Map>): +// Map implements Sload as identity (no actual SLOAD — returns the slot as-is). +// So `outer.get(k1)` derives a slot and "loads" the inner Map, which is just +// that derived slot. Then `.get(k2)` derives again and SLOADs the leaf value. +// +// Requirements: +// - Keys must implement Hash (derive_slot: key + base_slot → keccak256 → new slot) +// - Leaf values must implement Sload + Sstore (actual SLOAD/SSTORE opcodes) +// - Map values implement Sload as identity (no SLOAD, slot passthrough) +// +// Usage: +// use std::map::Map; +// +// contract MyContract { +// let balances: &s Map; +// let allowances: &s Map>; +// +// fn get_balance(key: u256) -> u256 { +// self.balances.get(key) +// } +// +// fn get_allowance(owner: address, spender: address) -> u256 { +// self.allowances.get(owner).get(spender) +// } +// +// fn set_balance(key: u256, val: u256) { +// self.balances.set(key, val); +// } +// } + +use std::ops::UniqueSlot; +use std::ops::Sload; +use std::ops::Sstore; + +// Map is just a u256 at runtime — the base slot number. +// Contract fields declared as `let m: &s Map` evaluate to +// the slot constant (no SLOAD). Map is a zero-storage type. +type Map = (); + +impl Map { + // Derive the storage slot for `key` and load the value. + // For leaf V (u256, etc): V::sload does an actual SLOAD. + // For nested V (Map): V::sload returns the slot as-is (identity). + fn get(self, key: K) -> (V) { + let slot: u256 = key.derive_slot(self); + V::sload(slot) + } + + // Derive the storage slot for `key` and store the value. + fn set(self, key: K, val: V) { + let slot: u256 = key.derive_slot(self); + val.sstore(slot); + } +} + +// Map implements Sload as identity — "loading" a Map from a slot +// just returns the slot itself. No actual SLOAD is emitted. +// This is what makes nested maps (Map>) work: +// the outer get() derives a slot and passes it through as the +// inner Map's base slot, without touching storage. +impl Map: Sload { + fn sload(slot: u256) -> Self { + slot + } +} + +// Map implements Sstore as a no-op — storing a Map to a slot does nothing. +// This exists to satisfy the V: Sstore bound on nested Map> +// declarations. In practice, individual leaf values are stored directly. +impl Map: Sstore { + fn sstore(self, slot: u256) { + // No-op: Map values don't get stored to individual slots. + // Nested maps derive slots and store leaf values directly. + } +} + +// Allow for map[key] +impl Map: Index { + fn index(self, index: K) -> (V) { + self.get(index) + } +} diff --git a/std/ops.edge b/std/globals/ops.edge similarity index 67% rename from std/ops.edge rename to std/globals/ops.edge index b06e800..247586f 100644 --- a/std/ops.edge +++ b/std/globals/ops.edge @@ -56,3 +56,24 @@ trait UnsafeSub { trait UnsafeMul { fn unsafe_mul(self, rhs: Self) -> (Self); } + +// Index trait allows for operator overloading of indexing, i.e. my_map[a][b] or my_array[0] +// using a custom index and output. +trait Index { + fn index(self, index: Idx) -> (Output); +} + +trait UniqueSlot { + // Derive a storage slot from this key and a base slot. + // EVM convention: keccak256(abi.encode(key, base_slot)) + // Compiler provides implementations for primitive types. + fn derive_slot(self, base_slot: u256) -> u256; +} + +trait Sstore { + fn sstore(self, base_slot: u256); +} + +trait Sload { + fn sload(base_slot: u256) -> Self; +} diff --git a/std/globals/option.edge b/std/globals/option.edge new file mode 100644 index 0000000..b2695fc --- /dev/null +++ b/std/globals/option.edge @@ -0,0 +1 @@ +type Option = None | Some(T); diff --git a/std/globals/result.edge b/std/globals/result.edge new file mode 100644 index 0000000..a1dd1f7 --- /dev/null +++ b/std/globals/result.edge @@ -0,0 +1 @@ +type Result = Ok(T) | Err(E); diff --git a/std/patterns/factory.edge b/std/patterns/factory.edge index 5484fd3..e1b7f06 100644 --- a/std/patterns/factory.edge +++ b/std/patterns/factory.edge @@ -5,7 +5,7 @@ // - b32 type for salt/bytecode hashes // - @caller() builtin // - Events (Deployed) with indexed fields -// - map storage mapping for tracking deployments +// - Map storage mapping for tracking deployments // - Bitwise operators for address derivation // - Multiple return patterns // @@ -29,7 +29,7 @@ abi IFactory { contract Factory { // Mapping from salt to deployed contract address. - let deployments: &s map; + let deployments: &s Map; // Number of contracts deployed. let deploy_count: &s u256; diff --git a/std/patterns/timelock.edge b/std/patterns/timelock.edge index 867a479..eb2ca42 100644 --- a/std/patterns/timelock.edge +++ b/std/patterns/timelock.edge @@ -49,13 +49,13 @@ contract Timelock { let admin: &s addr; // Operation execution timestamps (0 = not scheduled, >0 = ready-at time). - let timestamps: &s map; + let timestamps: &s Map; // Whether an operation has been executed. - let executed: &s map; + let executed: &s Map; // Whether an operation has been cancelled. - let cancelled: &s map; + let cancelled: &s Map; // Schedule a new timelocked operation. pub fn schedule(id: b32, target: addr, value: u256, delay: u256) { diff --git a/std/tokens/erc1155.edge b/std/tokens/erc1155.edge index fc427b4..d44aaff 100644 --- a/std/tokens/erc1155.edge +++ b/std/tokens/erc1155.edge @@ -62,10 +62,10 @@ contract ERC1155 { // ── State ───────────────────────────────────────────────────────────────── // Balances: owner -> token ID -> amount. - let balances: &s map>; + let balances: &s Map>; // Operator approvals: owner -> operator -> approved. - let approval_for_all: &s map>; + let approval_for_all: &s Map>; // ── Public Read Functions ───────────────────────────────────────────────── diff --git a/std/tokens/erc20.edge b/std/tokens/erc20.edge index 581eca0..23ef72c 100644 --- a/std/tokens/erc20.edge +++ b/std/tokens/erc20.edge @@ -61,10 +61,10 @@ contract ERC20 { let total_supply: &s u256; // Balance of each account (address -> amount). - let balances: &s map; + let balances: &s Map; // Allowances: owner -> spender -> amount. - let allowances: &s map>; + let allowances: &s Map>; // ── Public Read Functions ───────────────────────────────────────────────── diff --git a/std/tokens/weth.edge b/std/tokens/weth.edge index 1d2431e..74e3c99 100644 --- a/std/tokens/weth.edge +++ b/std/tokens/weth.edge @@ -6,7 +6,7 @@ // - @caller() and @callvalue() builtins // - pub/fn function visibility // - emit keyword for logging events -// - map storage mapping +// - Map storage mapping // // Usage: // use std::tokens::weth::IWETH; @@ -38,10 +38,10 @@ contract WETH { let total_supply: &s u256; // Balance of each account (address -> amount). - let balances: &s map; + let balances: &s Map; // Allowances: owner -> spender -> amount. - let allowances: &s map>; + let allowances: &s Map>; // Deposit ETH and mint equivalent WETH to the caller. pub fn deposit() { From decb3210e32ec3f05282ac3007b954f3f232a185 Mon Sep 17 00:00:00 2001 From: brockelmore <31553173+brockelmore@users.noreply.github.com> Date: Wed, 11 Mar 2026 11:29:34 -0700 Subject: [PATCH 3/5] =?UTF-8?q?refactor:=20simplify=20calls.rs=20=E2=80=94?= =?UTF-8?q?=20extract=20helpers,=20remove=20StructParamInfo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extract `lookup_binding_for_expr()` to deduplicate scope traversal in `infer_receiver_type` and `infer_receiver_type_args` - Extract `try_compiler_stateful_dispatch()` to deduplicate the lower-receiver + lower-args + compiler_provided_stateful_method pattern - Replace `StructParamInfo` struct with simple `(String, usize)` tuple — callsite only needs name and field count, no need to clone fields Co-Authored-By: Claude Opus 4.6 --- crates/ir/src/to_egglog/calls.rs | 130 +++++++++++++--------------- crates/ir/src/to_egglog/function.rs | 25 ++---- 2 files changed, 65 insertions(+), 90 deletions(-) diff --git a/crates/ir/src/to_egglog/calls.rs b/crates/ir/src/to_egglog/calls.rs index 16cf81d..ccac6cc 100644 --- a/crates/ir/src/to_egglog/calls.rs +++ b/crates/ir/src/to_egglog/calls.rs @@ -274,17 +274,10 @@ impl AstToEgglog { } // Check compiler-provided stateful methods (derive_slot, sload, sstore) + if let Some(result) = + self.try_compiler_stateful_dispatch(receiver, method_name, args)? { - let recv_ir = self.lower_expr(receiver)?; - let args_ir: Vec = args - .iter() - .map(|a| self.lower_expr(a)) - .collect::>()?; - if let Some(result) = - self.compiler_provided_stateful_method(method_name, Some(recv_ir), &args_ir) - { - return Ok(result); - } + return Ok(result); } } @@ -351,17 +344,10 @@ impl AstToEgglog { } // Also check stateful methods for unknown receiver + if let Some(result) = + self.try_compiler_stateful_dispatch(receiver, method_name, args)? { - let recv_ir = self.lower_expr(receiver)?; - let args_ir: Vec = args - .iter() - .map(|a| self.lower_expr(a)) - .collect::>()?; - if let Some(result) = - self.compiler_provided_stateful_method(method_name, Some(recv_ir), &args_ir) - { - return Ok(result); - } + return Ok(result); } } @@ -619,22 +605,42 @@ impl AstToEgglog { } /// Infer the type of a receiver expression (best-effort). - pub(crate) fn infer_receiver_type(&self, expr: &edge_ast::Expr) -> Option { - match expr { - edge_ast::Expr::Ident(ident) => { - for scope in self.scopes.iter().rev() { - if let Some(binding) = scope.bindings.get(&ident.name) { - // Composite type (struct/union/array) takes priority - if let Some(ref ct) = binding.composite_type { - return Some(ct.clone()); - } - // Fall back to primitive type name from EvmType - let result = Self::evm_type_to_name(&binding._ty); - return result; + /// Look up the scope binding for an expression (Ident or self.field). + /// Returns the variable name and binding reference if found. + fn lookup_binding_for_expr<'a>(&'a self, expr: &edge_ast::Expr) -> Option<&'a super::VarBinding> { + let var_name = match expr { + edge_ast::Expr::Ident(ident) => &ident.name, + edge_ast::Expr::FieldAccess(obj, field, _) => { + if let edge_ast::Expr::Ident(ident) = obj.as_ref() { + if ident.name == "self" { + &field.name + } else { + return None; } + } else { + return None; } - None } + _ => return None, + }; + for scope in self.scopes.iter().rev() { + if let Some(binding) = scope.bindings.get(var_name) { + return Some(binding); + } + } + None + } + + pub(crate) fn infer_receiver_type(&self, expr: &edge_ast::Expr) -> Option { + // Try direct binding lookup first + if let Some(binding) = self.lookup_binding_for_expr(expr) { + if let Some(ref ct) = binding.composite_type { + return Some(ct.clone()); + } + return Self::evm_type_to_name(&binding._ty); + } + + match expr { edge_ast::Expr::StructInstantiation(_, type_name, _, _) => Some(type_name.name.clone()), edge_ast::Expr::Literal(lit) => match lit.as_ref() { edge_ast::Lit::Bool(_, _) => Some("bool".to_string()), @@ -644,30 +650,12 @@ impl AstToEgglog { edge_ast::Lit::Int(_, None, _) => Some("u256".to_string()), _ => None, }, - // FieldAccess on self: `self.field` — look up the field binding - edge_ast::Expr::FieldAccess(obj, field, _) => { - if let edge_ast::Expr::Ident(ident) = obj.as_ref() { - if ident.name == "self" { - // Look up the field in scope - for scope in self.scopes.iter().rev() { - if let Some(binding) = scope.bindings.get(&field.name) { - if let Some(ref ct) = binding.composite_type { - return Some(ct.clone()); - } - return Self::evm_type_to_name(&binding._ty); - } - } - } - } - None - } // ArrayIndex: base[index] — if base is a Map, the result type is the value type (V) edge_ast::Expr::ArrayIndex(base, _, _, _) => { let base_type = self.infer_receiver_type(base); let base_args = self.infer_receiver_type_args(base); if let Some(ref bt) = base_type { if bt.starts_with("Map") && base_args.len() == 2 { - // V is the second type arg — use mangled name return Some(Self::type_sig_mangle(&base_args[1])); } } @@ -679,27 +667,11 @@ impl AstToEgglog { /// Get the concrete type arguments for a receiver's generic composite type. pub(crate) fn infer_receiver_type_args(&self, expr: &edge_ast::Expr) -> Vec { + if let Some(binding) = self.lookup_binding_for_expr(expr) { + return binding.composite_type_args.clone(); + } + match expr { - edge_ast::Expr::Ident(ident) => { - for scope in self.scopes.iter().rev() { - if let Some(binding) = scope.bindings.get(&ident.name) { - return binding.composite_type_args.clone(); - } - } - Vec::new() - } - edge_ast::Expr::FieldAccess(obj, field, _) => { - if let edge_ast::Expr::Ident(ident) = obj.as_ref() { - if ident.name == "self" { - for scope in self.scopes.iter().rev() { - if let Some(binding) = scope.bindings.get(&field.name) { - return binding.composite_type_args.clone(); - } - } - } - } - Vec::new() - } // ArrayIndex: base[index] — if base is a Map, the result's type args come from V edge_ast::Expr::ArrayIndex(base, _, _, _) => { let base_args = self.infer_receiver_type_args(base); @@ -806,6 +778,22 @@ impl AstToEgglog { /// Compiler-provided complex trait methods for primitive types. /// Unlike `compiler_provided_method` (simple binary ops), these produce + /// Lower receiver + args and try compiler-provided stateful method dispatch. + /// Used for `.derive_slot()`, `.sload()`, `.sstore()` on primitives. + fn try_compiler_stateful_dispatch( + &mut self, + receiver: &edge_ast::Expr, + method_name: &str, + args: &[edge_ast::Expr], + ) -> Result, IrError> { + let recv_ir = self.lower_expr(receiver)?; + let args_ir: Vec = args + .iter() + .map(|a| self.lower_expr(a)) + .collect::>()?; + Ok(self.compiler_provided_stateful_method(method_name, Some(recv_ir), &args_ir)) + } + /// full IR expression trees with state threading. /// /// Returns `Some(ir_expr)` if the method was handled, `None` otherwise. diff --git a/crates/ir/src/to_egglog/function.rs b/crates/ir/src/to_egglog/function.rs index edbc925..a12ff25 100644 --- a/crates/ir/src/to_egglog/function.rs +++ b/crates/ir/src/to_egglog/function.rs @@ -75,9 +75,8 @@ impl AstToEgglog { .bindings .insert(ident.name.clone(), binding); calldata_offset += n * 32; - } else if let Some(struct_info) = self.resolve_struct_param_type(type_sig) { + } else if let Some((struct_name, n_fields)) = self.resolve_struct_param_type(type_sig) { // Struct parameter: allocate memory and copy fields from calldata - let n_fields = struct_info.fields.len(); let base_ir = self.alloc_region(n_fields); // Copy each field from calldata to memory @@ -93,7 +92,7 @@ impl AstToEgglog { storage_slot: None, _ty: ty, let_bind_name: None, - composite_type: Some(struct_info.name), + composite_type: Some(struct_name), composite_base: Some(base_ir), composite_type_args: Vec::new(), }; @@ -608,11 +607,11 @@ impl AstToEgglog { } /// Check if a parameter type sig resolves to a known struct type. - /// Returns (struct_name, field_count) if so. + /// Returns `(struct_name, field_count)` if so. pub(crate) fn resolve_struct_param_type( &self, type_sig: &edge_ast::ty::TypeSig, - ) -> Option { + ) -> Option<(String, usize)> { let resolved = self.resolve_type_alias(type_sig); let name = match resolved { edge_ast::ty::TypeSig::Named(ident, _) => &ident.name, @@ -621,28 +620,16 @@ impl AstToEgglog { // Direct lookup if let Some(info) = self.struct_types.get(name.as_str()) { - return Some(StructParamInfo { - name: name.clone(), - fields: info.fields.clone(), - }); + return Some((name.clone(), info.fields.len())); } // Try resolving through type_param_subst (for generic params like V → CustomSStore) if let Some(resolved_name) = self.type_param_subst.get(name.as_str()) { if let Some(info) = self.struct_types.get(resolved_name.as_str()) { - return Some(StructParamInfo { - name: resolved_name.clone(), - fields: info.fields.clone(), - }); + return Some((resolved_name.clone(), info.fields.len())); } } None } } - -/// Info about a struct-typed function parameter. -pub(crate) struct StructParamInfo { - pub name: String, - pub fields: Vec<(String, EvmType)>, -} From 0552552e2c519964e8f1fa6d2350996e3bd3a260 Mon Sep 17 00:00:00 2001 From: brockelmore <31553173+brockelmore@users.noreply.github.com> Date: Wed, 11 Mar 2026 12:12:41 -0700 Subject: [PATCH 4/5] refactor: replace hardcoded Map checks with Index trait lookup, fix generics soundness - Replace starts_with("Map") checks in infer_receiver_type, infer_receiver_type_args, and inline_function_call with Index trait impl Output type lookup - Add trait_type_args to TraitImplInfo and GenericImplBlock for type-system-based dispatch - Fix is_primitive_type to validate width ranges (8..=256 step 8) matching lexer rules - Fix resolve_generic_type_name to return None on ambiguous multiple monomorphizations - Add resolve_generic_type_name_with_args for precise resolution with type context - Add type_sig_hint threading from VarDecl to struct instantiation for disambiguation - Fix composite_type_args propagation in inline_function_call (was dropping to Vec::new) - Improve error messages: "ambiguous generic type" instead of "unknown" for multi-monomorph Co-Authored-By: Claude Opus 4.6 --- crates/ir/src/to_egglog/calls.rs | 93 +++++++++++++++++++--------- crates/ir/src/to_egglog/composite.rs | 50 ++++++++++++--- crates/ir/src/to_egglog/expr.rs | 3 + crates/ir/src/to_egglog/mod.rs | 23 +++++++ crates/ir/src/to_egglog/types.rs | 58 +++++++++++++++-- 5 files changed, 187 insertions(+), 40 deletions(-) diff --git a/crates/ir/src/to_egglog/calls.rs b/crates/ir/src/to_egglog/calls.rs index ccac6cc..6bb548a 100644 --- a/crates/ir/src/to_egglog/calls.rs +++ b/crates/ir/src/to_egglog/calls.rs @@ -650,13 +650,12 @@ impl AstToEgglog { edge_ast::Lit::Int(_, None, _) => Some("u256".to_string()), _ => None, }, - // ArrayIndex: base[index] — if base is a Map, the result type is the value type (V) + // ArrayIndex: base[index] — if base implements Index, the result type is Index::Output edge_ast::Expr::ArrayIndex(base, _, _, _) => { let base_type = self.infer_receiver_type(base); - let base_args = self.infer_receiver_type_args(base); if let Some(ref bt) = base_type { - if bt.starts_with("Map") && base_args.len() == 2 { - return Some(Self::type_sig_mangle(&base_args[1])); + if let Some(output) = self.index_output_type(bt) { + return Some(output); } } None @@ -665,6 +664,28 @@ impl AstToEgglog { } } + /// Look up the Index trait impl's Output type for a given type name. + /// Returns the mangled name of the Output type if the type implements Index. + pub(crate) fn index_output_type(&self, type_name: &str) -> Option { + if let Some(impl_info) = self.trait_impls.get(&(type_name.to_string(), "Index".to_string())) { + // Index — Output is the second type arg + if impl_info.trait_type_args.len() >= 2 { + return Some(Self::type_sig_mangle(&impl_info.trait_type_args[1])); + } + } + None + } + + /// Look up the Index trait impl's Output TypeSig for a given type name. + pub(crate) fn index_output_type_sig(&self, type_name: &str) -> Option { + if let Some(impl_info) = self.trait_impls.get(&(type_name.to_string(), "Index".to_string())) { + if impl_info.trait_type_args.len() >= 2 { + return Some(impl_info.trait_type_args[1].clone()); + } + } + None + } + /// Get the concrete type arguments for a receiver's generic composite type. pub(crate) fn infer_receiver_type_args(&self, expr: &edge_ast::Expr) -> Vec { if let Some(binding) = self.lookup_binding_for_expr(expr) { @@ -672,12 +693,14 @@ impl AstToEgglog { } match expr { - // ArrayIndex: base[index] — if base is a Map, the result's type args come from V + // ArrayIndex: base[index] — result's type args come from Index::Output edge_ast::Expr::ArrayIndex(base, _, _, _) => { - let base_args = self.infer_receiver_type_args(base); - if base_args.len() == 2 { - if let edge_ast::ty::TypeSig::Named(_, inner_args) = &base_args[1] { - return inner_args.clone(); + let base_type = self.infer_receiver_type(base); + if let Some(ref bt) = base_type { + if let Some(output_sig) = self.index_output_type_sig(bt) { + if let edge_ast::ty::TypeSig::Named(_, inner_args) = &output_sig { + return inner_args.clone(); + } } } Vec::new() @@ -752,17 +775,13 @@ impl AstToEgglog { /// Check if a type name refers to a primitive type (not a user-defined composite). pub(crate) fn is_primitive_type(type_name: &str) -> bool { - type_name == "u256" - || type_name == "i256" - || type_name == "bool" - || type_name == "address" - || type_name == "b32" - || type_name.starts_with("u") - && type_name[1..].parse::().is_ok() - || type_name.starts_with("i") - && type_name[1..].parse::().is_ok() - || type_name.starts_with("bytes") - && type_name[5..].parse::().is_ok() + matches!(type_name, "bool" | "address" | "b32" | "bit") + || type_name.strip_prefix('u').and_then(|s| s.parse::().ok()) + .is_some_and(|w| (8..=256).contains(&w) && w % 8 == 0) + || type_name.strip_prefix('i').and_then(|s| s.parse::().ok()) + .is_some_and(|w| (8..=256).contains(&w) && w % 8 == 0) + || type_name.strip_prefix("bytes").and_then(|s| s.parse::().ok()) + .is_some_and(|n| (1..=32).contains(&n)) } /// Look up a compiler-provided trait method for a primitive type. @@ -1029,7 +1048,11 @@ impl AstToEgglog { if let edge_ast::Expr::Ident(ident) = arg { let info = self.lookup_composite_info(&ident.name); if let Some((ct, cb)) = info { - arg_composite.push(Some((ct, Some(cb), Vec::new()))); + // Also grab composite_type_args from the binding + let type_args = self.lookup_binding_for_expr(arg) + .map(|b| b.composite_type_args.clone()) + .unwrap_or_default(); + arg_composite.push(Some((ct, Some(cb), type_args))); } else { // Check for composite_type without composite_base (e.g., Map type aliases) let mut found = false; @@ -1048,14 +1071,12 @@ impl AstToEgglog { } } else if let edge_ast::Expr::ArrayIndex(base, _, _, _) = arg { // For ArrayIndex args (e.g., map[key] as self parameter), - // infer the value type from the base Map's type args. + // infer the value type from the base type's Index impl Output. let base_type = self.infer_receiver_type(base); - let base_args = self.infer_receiver_type_args(base); if let Some(ref bt) = base_type { - if bt.starts_with("Map") && base_args.len() == 2 { - let value_mangled = Self::type_sig_mangle(&base_args[1]); - // Extract inner type args if V is a generic type - let inner_args = if let edge_ast::ty::TypeSig::Named(_, inner) = &base_args[1] { + if let Some(output_sig) = self.index_output_type_sig(bt) { + let value_mangled = Self::type_sig_mangle(&output_sig); + let inner_args = if let edge_ast::ty::TypeSig::Named(_, inner) = &output_sig { inner.clone() } else { Vec::new() @@ -1079,7 +1100,7 @@ impl AstToEgglog { .get(i) .cloned() .unwrap_or_else(|| ast_helpers::const_int(0, self.current_ctx.clone())); - let (mut composite_type, mut composite_base, composite_type_args) = arg_composite + let (mut composite_type, mut composite_base, mut composite_type_args) = arg_composite .get(i) .and_then(|c| c.as_ref()) .map(|(ct, cb, ta)| (Some(ct.clone()), cb.clone(), ta.clone())) @@ -1113,6 +1134,10 @@ impl AstToEgglog { || self.union_types.contains_key(&resolved_name) { composite_type = Some(resolved_name); + // Propagate type args from the param type sig + if composite_type_args.is_empty() && !type_args.is_empty() { + composite_type_args = type_args.clone(); + } } else if type_args.is_empty() { // Check if resolved name is a generic type that was // monomorphized (e.g., Result__u256) @@ -1122,6 +1147,18 @@ impl AstToEgglog { { composite_type = Some(mangled); } + } else { + // Named type with type args — try monomorphizing + if let Ok(Some(mangled)) = self.try_monomorphize_named_type( + &resolved_name, + type_args, + None, + ) { + composite_type = Some(mangled); + if composite_type_args.is_empty() { + composite_type_args = type_args.clone(); + } + } } } } diff --git a/crates/ir/src/to_egglog/composite.rs b/crates/ir/src/to_egglog/composite.rs index d690043..c7c4654 100644 --- a/crates/ir/src/to_egglog/composite.rs +++ b/crates/ir/src/to_egglog/composite.rs @@ -33,8 +33,17 @@ impl AstToEgglog { }) })? } else { - let diag = - edge_diagnostics::Diagnostic::error(format!("unknown union type: `{type_name}`")); + // Check if resolution failed due to ambiguity (multiple monomorphizations) + let candidate_count = self.monomorphized_types.iter() + .filter(|((base, _), _)| base == type_name) + .count(); + let diag = if candidate_count > 1 { + edge_diagnostics::Diagnostic::error(format!( + "ambiguous generic type `{type_name}`: {candidate_count} monomorphizations exist", + )).with_note("provide explicit type arguments to disambiguate") + } else { + edge_diagnostics::Diagnostic::error(format!("unknown union type: `{type_name}`")) + }; return Err(IrError::Diagnostic(if let Some(s) = span { diag.with_label(s.clone(), "not found") } else { @@ -74,9 +83,18 @@ impl AstToEgglog { type_name.to_string() } else { self.resolve_generic_type_name(type_name).ok_or_else(|| { - let diag = edge_diagnostics::Diagnostic::error(format!( - "unknown union type: `{type_name}`", - )); + let candidate_count = self.monomorphized_types.iter() + .filter(|((base, _), _)| base == type_name) + .count(); + let diag = if candidate_count > 1 { + edge_diagnostics::Diagnostic::error(format!( + "ambiguous generic type `{type_name}`: {candidate_count} monomorphizations exist", + )).with_note("provide explicit type arguments to disambiguate") + } else { + edge_diagnostics::Diagnostic::error(format!( + "unknown union type: `{type_name}`", + )) + }; IrError::Diagnostic(if let Some(s) = span { diag.with_label(s.clone(), "not found") } else { @@ -139,12 +157,28 @@ impl AstToEgglog { type_name: &str, fields: &[(edge_ast::Ident, edge_ast::Expr)], ) -> Result { - // Resolve generic struct names to monomorphized versions + // Resolve generic struct names to monomorphized versions. + // Use type_sig_hint from VarDecl annotation when available for precise resolution. let resolved_name = if self.struct_types.contains_key(type_name) { type_name.to_string() } else { - self.resolve_generic_type_name(type_name) - .unwrap_or_else(|| type_name.to_string()) + // Try precise resolution via type_sig_hint first + let from_hint = if let Some(edge_ast::ty::TypeSig::Named(ref hint_name, ref hint_args)) = self.type_sig_hint { + if (hint_name.name == type_name || hint_name.name.starts_with(type_name)) && !hint_args.is_empty() { + self.resolve_generic_type_name_with_args(type_name, hint_args) + } else { + None + } + } else { + None + }; + if let Some(resolved) = from_hint { + resolved + } else { + // Fall back to unambiguous resolution + self.resolve_generic_type_name(type_name) + .unwrap_or_else(|| type_name.to_string()) + } }; let struct_info = self.struct_types.get(&resolved_name).cloned(); diff --git a/crates/ir/src/to_egglog/expr.rs b/crates/ir/src/to_egglog/expr.rs index a2bdbd7..5087983 100644 --- a/crates/ir/src/to_egglog/expr.rs +++ b/crates/ir/src/to_egglog/expr.rs @@ -143,7 +143,10 @@ impl AstToEgglog { // If there's an initializer, emit VarStore for the assignment if let Some(init) = init_expr { self.last_composite_alloc = None; + // Set type sig hint so struct instantiation can disambiguate generics + self.type_sig_hint = type_sig.as_ref().cloned(); let rhs_ir = self.lower_expr(init)?; + self.type_sig_hint = None; // Track composite type from RHS if applicable if let Some((comp_type, comp_base)) = self.last_composite_alloc.take() { if let Some(scope) = self.scopes.last_mut() { diff --git a/crates/ir/src/to_egglog/mod.rs b/crates/ir/src/to_egglog/mod.rs index d547afd..92c97ee 100644 --- a/crates/ir/src/to_egglog/mod.rs +++ b/crates/ir/src/to_egglog/mod.rs @@ -143,6 +143,8 @@ pub(crate) struct GenericTypeTemplate { pub(crate) struct GenericImplBlock { pub type_params: Vec, pub trait_impl: Option, // trait name, or None for inherent impl + /// The trait's type arguments (e.g., `[K, V]` for `impl Foo: Index`) + pub trait_type_params: Vec, pub items: Vec, } @@ -246,6 +248,8 @@ pub(crate) struct TraitInfo { #[derive(Debug, Clone)] pub(crate) struct TraitImplInfo { pub methods: IndexMap, + /// Trait type arguments from the impl declaration (e.g., `[K, V]` for `impl Foo: Index`). + pub trait_type_args: Vec, pub span: edge_types::span::Span, } @@ -332,6 +336,9 @@ pub struct AstToEgglog { /// Type hint from assignment target, used for generic return-type inference. /// Set before lowering the RHS of a typed variable assignment, cleared after. pub(crate) type_hint: Option, + /// TypeSig hint from assignment target, used to disambiguate generic struct instantiation. + /// Set before lowering the RHS of a typed variable declaration, cleared after. + pub(crate) type_sig_hint: Option, /// Compiler warnings collected during lowering pub(crate) warnings: Vec, } @@ -382,6 +389,7 @@ impl AstToEgglog { _self_type: None, std_ops_traits: HashSet::new(), type_hint: None, + type_sig_hint: None, warnings: Vec::new(), } } @@ -514,6 +522,7 @@ impl AstToEgglog { (prim.to_string(), trait_name.to_string()), TraitImplInfo { methods: IndexMap::new(), + trait_type_args: Vec::new(), span: edge_types::span::Span::EOF, }, ); @@ -689,12 +698,16 @@ impl AstToEgglog { || self.generic_type_templates.contains_key(&type_name) { let trait_name = impl_block.trait_impl.as_ref().map(|(n, _)| n.name.clone()); + let trait_type_params = impl_block.trait_impl.as_ref() + .map(|(_, params)| params.clone()) + .unwrap_or_default(); self.generic_impl_blocks .entry(type_name.clone()) .or_default() .push(GenericImplBlock { type_params: impl_block.type_params.clone(), trait_impl: trait_name, + trait_type_params, items: impl_block.items.clone(), }); } @@ -753,10 +766,20 @@ impl AstToEgglog { } } + // Extract trait type args from the impl declaration + let trait_type_args: Vec = impl_block.trait_impl + .as_ref() + .map(|(_, params)| { + params.iter() + .map(|p| edge_ast::ty::TypeSig::Named(p.name.clone(), Vec::new())) + .collect() + }) + .unwrap_or_default(); self.trait_impls.insert( (type_name, trait_name.name.clone()), TraitImplInfo { methods, + trait_type_args, span: impl_block.span.clone(), }, ); diff --git a/crates/ir/src/to_egglog/types.rs b/crates/ir/src/to_egglog/types.rs index c883988..3ec7ee2 100644 --- a/crates/ir/src/to_egglog/types.rs +++ b/crates/ir/src/to_egglog/types.rs @@ -41,20 +41,62 @@ impl AstToEgglog { } /// Resolve a generic type name (e.g., "Result") to its monomorphized name (e.g., "`Result__u256`"). - /// Searches `union_types` and `struct_types` for any key starting with `"{name}__"`. - /// Returns the first match found. + /// Returns `Some` only when there's exactly one monomorphization (unambiguous). + /// When there are multiple, returns `None` — caller should use + /// `resolve_generic_type_name_with_args` for precise resolution. pub(crate) fn resolve_generic_type_name(&self, name: &str) -> Option { + // Check monomorphized_types cache for entries with this base name + let candidates: Vec<&String> = self.monomorphized_types.iter() + .filter(|((base, _), _)| base == name) + .map(|(_, mangled)| mangled) + .collect(); + if candidates.len() == 1 { + return Some(candidates[0].clone()); + } + if candidates.len() > 1 { + // Multiple monomorphizations — ambiguous, return None + return None; + } + + // Fallback: scan union_types and struct_types for "{name}__" prefix, + // but only return if unambiguous. + let mut fallback_candidates = Vec::new(); let prefix = format!("{name}__"); for key in self.union_types.keys() { if key.starts_with(&prefix) { - return Some(key.clone()); + fallback_candidates.push(key.clone()); } } for key in self.struct_types.keys() { if key.starts_with(&prefix) { - return Some(key.clone()); + fallback_candidates.push(key.clone()); } } + if fallback_candidates.len() == 1 { + return Some(fallback_candidates.into_iter().next().unwrap()); + } + None + } + + /// Resolve a generic type name with specific type args to its monomorphized name. + /// More precise than `resolve_generic_type_name` when multiple monomorphizations exist. + pub(crate) fn resolve_generic_type_name_with_args( + &self, + name: &str, + type_args: &[edge_ast::ty::TypeSig], + ) -> Option { + let mangled_args: Vec = type_args.iter() + .map(|a| Self::type_sig_mangle(a)) + .collect(); + let cache_key = (name.to_string(), mangled_args); + if let Some(mangled) = self.monomorphized_types.get(&cache_key) { + return Some(mangled.clone()); + } + // Fallback: construct the expected mangled name and check if it exists + let expected = format!("{}_{}", name, cache_key.1.join("_")); + if self.union_types.contains_key(&expected) || self.struct_types.contains_key(&expected) { + return Some(expected); + } None } @@ -662,10 +704,18 @@ impl AstToEgglog { methods.insert(fn_decl.name.name.clone(), (fn_decl.clone(), body.clone())); } } + // Substitute type params in trait type args to get concrete types + let trait_type_args: Vec = gib.trait_type_params.iter() + .map(|p| { + let sig = edge_ast::ty::TypeSig::Named(p.name.clone(), Vec::new()); + Self::substitute_type_params(&sig, &impl_subst) + }) + .collect(); self.trait_impls.insert( (mangled.clone(), trait_name.clone()), super::TraitImplInfo { methods, + trait_type_args, span: edge_types::span::Span::EOF, }, ); From c5e12384781b5ab979f5502d36e765a6b84b3953 Mon Sep 17 00:00:00 2001 From: brockelmore <31553173+brockelmore@users.noreply.github.com> Date: Wed, 11 Mar 2026 12:19:32 -0700 Subject: [PATCH 5/5] chore: fix clippy warnings and format Co-Authored-By: Claude Opus 4.6 --- bin/edgec/src/main.rs | 6 +- crates/driver/src/compiler.rs | 32 +-- crates/e2e/tests/suites/map_std_exec.rs | 126 +++++++----- crates/ir/src/lib.rs | 2 +- crates/ir/src/to_egglog/calls.rs | 161 +++++++++------ crates/ir/src/to_egglog/composite.rs | 60 +++--- crates/ir/src/to_egglog/control_flow.rs | 4 +- crates/ir/src/to_egglog/expr.rs | 46 +++-- crates/ir/src/to_egglog/function.rs | 5 +- crates/ir/src/to_egglog/mod.rs | 48 +++-- crates/ir/src/to_egglog/pattern.rs | 22 +- crates/ir/src/to_egglog/storage.rs | 12 +- crates/ir/src/to_egglog/types.rs | 257 +++++++++++++----------- crates/parser/src/parser.rs | 3 +- 14 files changed, 447 insertions(+), 337 deletions(-) diff --git a/bin/edgec/src/main.rs b/bin/edgec/src/main.rs index 86c6899..ffb3a59 100644 --- a/bin/edgec/src/main.rs +++ b/bin/edgec/src/main.rs @@ -20,11 +20,7 @@ fn main() -> Result<()> { if let Some(level) = level { use tracing_subscriber::EnvFilter; // Egglog is extremely noisy — only enable at verbosity 5+ (-vvvvv) - let egglog_level = if cli.verbose >= 5 { - "trace" - } else { - "warn" - }; + let egglog_level = if cli.verbose >= 5 { "trace" } else { "warn" }; let filter = format!("edge={level},egglog={egglog_level},{level}"); tracing_subscriber::fmt() .with_env_filter(EnvFilter::new(filter)) diff --git a/crates/driver/src/compiler.rs b/crates/driver/src/compiler.rs index 332ec86..d30a051 100644 --- a/crates/driver/src/compiler.rs +++ b/crates/driver/src/compiler.rs @@ -365,16 +365,14 @@ impl Compiler { /// Run the parser and produce an AST fn parse(&mut self) -> Result { let mut parser = Parser::new(&self.session.source).map_err(|e| { - self.session - .emit_error(Self::parse_error_to_diagnostic(&e)); + self.session.emit_error(Self::parse_error_to_diagnostic(&e)); CompileError::ParseErrors })?; match parser.parse() { Ok(program) => Ok(program), Err(e) => { - self.session - .emit_error(Self::parse_error_to_diagnostic(&e)); + self.session.emit_error(Self::parse_error_to_diagnostic(&e)); self.session.report_diagnostics(); Err(CompileError::ParseErrors) } @@ -565,7 +563,12 @@ impl Compiler { /// traits, impls, functions) to the AST. fn auto_import_globals(&mut self, ast: &mut Program) -> Result<(), CompileError> { // Order matters: ops first (trait defs), then map (uses ops traits). - let global_keys = ["globals/ops", "globals/option", "globals/result", "globals/map"]; + let global_keys = [ + "globals/ops", + "globals/option", + "globals/result", + "globals/map", + ]; let mut new_stmts: Vec = Vec::new(); // Canonicalize the explicit override path once (if provided). @@ -581,12 +584,13 @@ impl Compiler { for key in &global_keys { let segments: Vec = key.split('/').map(String::from).collect(); - let source = if let Some(ref std_path) = explicit_std_path { - Self::try_read_from_fs(std_path, &segments) - .or_else(|| Self::try_read_from_embedded(&segments).map(String::from)) - } else { - Self::try_read_from_embedded(&segments).map(String::from) - }; + let source = explicit_std_path.as_ref().map_or_else( + || Self::try_read_from_embedded(&segments).map(String::from), + |std_path| { + Self::try_read_from_fs(std_path, &segments) + .or_else(|| Self::try_read_from_embedded(&segments).map(String::from)) + }, + ); let Some(source) = source else { // Globals not available (e.g., downstream consumer without std/). @@ -645,11 +649,7 @@ impl Compiler { } _ => None, }; - if let Some(n) = name { - !user_defined.contains(n) - } else { - true - } + name.is_none_or(|n| !user_defined.contains(n)) }); new_stmts.append(&mut ast.stmts); diff --git a/crates/e2e/tests/suites/map_std_exec.rs b/crates/e2e/tests/suites/map_std_exec.rs index 06ea7b4..5d6e957 100644 --- a/crates/e2e/tests/suites/map_std_exec.rs +++ b/crates/e2e/tests/suites/map_std_exec.rs @@ -2,9 +2,9 @@ //! Execution-level tests for the std Map type. //! -//! Tests compile test_map_std.edge, deploy on in-memory revm, and verify +//! Tests compile `test_map_std.edge`, deploy on in-memory revm, and verify //! basic Map get/set, index operators, direct custom storage, and -//! Map with user-defined Sload/Sstore impls. +//! `Map` with user-defined Sload/Sstore impls. use crate::helpers::*; @@ -66,10 +66,7 @@ fn test_custom_storage_set_then_get() { fn test_basic_map_get_initially_zero() { let bc = compile_contract(CONTRACT); let mut evm = EvmHandle::new(bc); - let r = evm.call(calldata( - selector("get_basic(uint256)"), - &[encode_u256(42)], - )); + let r = evm.call(calldata(selector("get_basic(uint256)"), &[encode_u256(42)])); assert!(r.success, "get_basic(42) reverted"); assert_eq!(decode_u256(&r.output), 0, "unset key should return 0"); } @@ -85,10 +82,7 @@ fn test_basic_map_set_then_get() { )); assert!(r.success, "set_basic(1, 999) reverted"); - let r = evm.call(calldata( - selector("get_basic(uint256)"), - &[encode_u256(1)], - )); + let r = evm.call(calldata(selector("get_basic(uint256)"), &[encode_u256(1)])); assert!(r.success, "get_basic(1) reverted"); assert_eq!(decode_u256(&r.output), 999, "get_basic(1) should be 999"); } @@ -110,17 +104,11 @@ fn test_basic_map_different_keys_independent() { )); assert!(r.success, "set_basic(20, 200) reverted"); - let r = evm.call(calldata( - selector("get_basic(uint256)"), - &[encode_u256(10)], - )); + let r = evm.call(calldata(selector("get_basic(uint256)"), &[encode_u256(10)])); assert!(r.success); assert_eq!(decode_u256(&r.output), 100); - let r = evm.call(calldata( - selector("get_basic(uint256)"), - &[encode_u256(20)], - )); + let r = evm.call(calldata(selector("get_basic(uint256)"), &[encode_u256(20)])); assert!(r.success); assert_eq!(decode_u256(&r.output), 200); } @@ -142,12 +130,13 @@ fn test_basic_map_overwrite() { )); assert!(r.success); - let r = evm.call(calldata( - selector("get_basic(uint256)"), - &[encode_u256(5)], - )); + let r = evm.call(calldata(selector("get_basic(uint256)"), &[encode_u256(5)])); assert!(r.success); - assert_eq!(decode_u256(&r.output), 222, "overwritten value should be 222"); + assert_eq!( + decode_u256(&r.output), + 222, + "overwritten value should be 222" + ); } // ============================================================================= @@ -188,10 +177,7 @@ fn test_basic_map_index_set() { assert!(r.success, "set_basic_by_indexable reverted"); // Read via .get() - let r = evm.call(calldata( - selector("get_basic(uint256)"), - &[encode_u256(3)], - )); + let r = evm.call(calldata(selector("get_basic(uint256)"), &[encode_u256(3)])); assert!(r.success, "get_basic reverted"); assert_eq!(decode_u256(&r.output), 333); } @@ -216,12 +202,13 @@ fn test_basic_map_index_interop() { assert_eq!(decode_u256(&r.output), 9999); // Also readable via .get() - let r = evm.call(calldata( - selector("get_basic(uint256)"), - &[encode_u256(99)], - )); + let r = evm.call(calldata(selector("get_basic(uint256)"), &[encode_u256(99)])); assert!(r.success); - assert_eq!(decode_u256(&r.output), 9999, ".get and index should read same slot"); + assert_eq!( + decode_u256(&r.output), + 9999, + ".get and index should read same slot" + ); } // ============================================================================= @@ -238,12 +225,13 @@ fn test_basic_map_index_interop() { fn test_custom_map_get_initially_zero() { let bc = compile_contract(CONTRACT); let mut evm = EvmHandle::new(bc); - let r = evm.call(calldata( - selector("get_custom(uint256)"), - &[encode_u256(1)], - )); + let r = evm.call(calldata(selector("get_custom(uint256)"), &[encode_u256(1)])); assert!(r.success, "get_custom(1) reverted"); - assert_eq!(decode_u256(&r.output), 0, "unset custom map key should be 0"); + assert_eq!( + decode_u256(&r.output), + 0, + "unset custom map key should be 0" + ); } #[test] @@ -285,7 +273,11 @@ fn test_double_custom_get_initially_zero() { &[encode_u256(1), encode_u256(2)], )); assert!(r.success, "get_double_custom(1,2) reverted"); - assert_eq!(decode_u256(&r.output), 0, "unset double custom key should return 0"); + assert_eq!( + decode_u256(&r.output), + 0, + "unset double custom key should return 0" + ); } #[test] @@ -296,7 +288,12 @@ fn test_double_custom_set_then_get() { // set_double_custom(a=1, b=2, val_a=100, val_b=200) let r = evm.call(calldata( selector("set_double_custom(uint128,uint128,uint128,uint128)"), - &[encode_u256(1), encode_u256(2), encode_u256(100), encode_u256(200)], + &[ + encode_u256(1), + encode_u256(2), + encode_u256(100), + encode_u256(200), + ], )); assert!(r.success, "set_double_custom reverted"); @@ -310,7 +307,10 @@ fn test_double_custom_set_then_get() { // Packed as (val_a << 128) | val_b in a u256 // val_a=100 in bytes 0..16, val_b=200 in bytes 16..32 let packed = &r.output[0..32]; - assert!(packed.iter().any(|&b| b != 0), "stored value should be non-zero"); + assert!( + packed.iter().any(|&b| b != 0), + "stored value should be non-zero" + ); } #[test] @@ -321,14 +321,24 @@ fn test_double_custom_different_keys_independent() { // Set key (1, 2) → val (10, 20) let r = evm.call(calldata( selector("set_double_custom(uint128,uint128,uint128,uint128)"), - &[encode_u256(1), encode_u256(2), encode_u256(10), encode_u256(20)], + &[ + encode_u256(1), + encode_u256(2), + encode_u256(10), + encode_u256(20), + ], )); assert!(r.success); // Set key (3, 4) → val (30, 40) let r = evm.call(calldata( selector("set_double_custom(uint128,uint128,uint128,uint128)"), - &[encode_u256(3), encode_u256(4), encode_u256(30), encode_u256(40)], + &[ + encode_u256(3), + encode_u256(4), + encode_u256(30), + encode_u256(40), + ], )); assert!(r.success); @@ -341,7 +351,11 @@ fn test_double_custom_different_keys_independent() { // Expected packed value: (10 << 128) | 20 // In big-endian 32 bytes: bytes[0..16] = 10, bytes[16..32] = 20 let expected_1_2 = pack_u128_pair(10, 20); - assert_eq!(&r.output[0..32], &expected_1_2[..], "key (1,2) should have val (10,20)"); + assert_eq!( + &r.output[0..32], + &expected_1_2[..], + "key (1,2) should have val (10,20)" + ); // Read key (3, 4) — should get val (30, 40) packed let r = evm.call(calldata( @@ -350,7 +364,11 @@ fn test_double_custom_different_keys_independent() { )); assert!(r.success); let expected_3_4 = pack_u128_pair(30, 40); - assert_eq!(&r.output[0..32], &expected_3_4[..], "key (3,4) should have val (30,40)"); + assert_eq!( + &r.output[0..32], + &expected_3_4[..], + "key (3,4) should have val (30,40)" + ); } #[test] @@ -361,14 +379,24 @@ fn test_double_custom_overwrite() { // Set key (5, 6) → val (50, 60) let r = evm.call(calldata( selector("set_double_custom(uint128,uint128,uint128,uint128)"), - &[encode_u256(5), encode_u256(6), encode_u256(50), encode_u256(60)], + &[ + encode_u256(5), + encode_u256(6), + encode_u256(50), + encode_u256(60), + ], )); assert!(r.success); // Overwrite key (5, 6) → val (55, 66) let r = evm.call(calldata( selector("set_double_custom(uint128,uint128,uint128,uint128)"), - &[encode_u256(5), encode_u256(6), encode_u256(55), encode_u256(66)], + &[ + encode_u256(5), + encode_u256(6), + encode_u256(55), + encode_u256(66), + ], )); assert!(r.success); @@ -379,7 +407,11 @@ fn test_double_custom_overwrite() { )); assert!(r.success); let expected = pack_u128_pair(55, 66); - assert_eq!(&r.output[0..32], &expected[..], "overwritten value should be (55,66)"); + assert_eq!( + &r.output[0..32], + &expected[..], + "overwritten value should be (55,66)" + ); } // ============================================================================= diff --git a/crates/ir/src/lib.rs b/crates/ir/src/lib.rs index 195e78f..1ef96bb 100644 --- a/crates/ir/src/lib.rs +++ b/crates/ir/src/lib.rs @@ -561,7 +561,7 @@ mod tests { "globals/map", ]; for key in &global_files { - let path = format!("../../std/{}.edge", key); + let path = format!("../../std/{key}.edge"); if let Ok(src) = std::fs::read_to_string(&path) { if let Ok(mut p) = edge_parser::Parser::new(&src) { if let Ok(globals_ast) = p.parse() { diff --git a/crates/ir/src/to_egglog/calls.rs b/crates/ir/src/to_egglog/calls.rs index 6bb548a..5bb6588 100644 --- a/crates/ir/src/to_egglog/calls.rs +++ b/crates/ir/src/to_egglog/calls.rs @@ -1,7 +1,6 @@ //! Function call lowering: call resolution, inlining, builtin calls. -use std::collections::HashMap; -use std::rc::Rc; +use std::{collections::HashMap, rc::Rc}; use super::{AstToEgglog, FreeFnInfo, Scope, VarBinding}; use crate::{ @@ -26,20 +25,35 @@ impl AstToEgglog { let type_name = &components[0].name; let variant_name = &components[1].name; if self.union_types.contains_key(type_name) { - return self.lower_union_instantiation_expr(type_name, variant_name, args, Some(span)); + return self.lower_union_instantiation_expr( + type_name, + variant_name, + args, + Some(span), + ); } // Check for generic union types (e.g., Result::Ok(42) where Result was monomorphized) if self.generic_type_templates.contains_key(type_name) { // First try to find an already-monomorphized version if let Some(mangled) = self.resolve_generic_type_name(type_name) { - return self.lower_union_instantiation_expr(&mangled, variant_name, args, Some(span)); + return self.lower_union_instantiation_expr( + &mangled, + variant_name, + args, + Some(span), + ); } // No monomorphized version yet — try to infer type params from // the constructor argument and monomorphize on the fly. if let Some(mangled) = self.try_monomorphize_union_from_constructor(type_name, variant_name, args)? { - return self.lower_union_instantiation_expr(&mangled, variant_name, args, Some(span)); + return self.lower_union_instantiation_expr( + &mangled, + variant_name, + args, + Some(span), + ); } return Err(IrError::Diagnostic( edge_diagnostics::Diagnostic::error(format!( @@ -64,7 +78,8 @@ impl AstToEgglog { if let edge_ast::Expr::Path(components, _) = callee { if components.len() == 2 { // Resolve type parameter substitutions (e.g., V → u256 inside Map methods) - let resolved_type = self.type_param_subst + let resolved_type = self + .type_param_subst .get(&components[0].name) .cloned() .unwrap_or_else(|| components[0].name.clone()); @@ -74,7 +89,10 @@ impl AstToEgglog { let method_span = &components[1].span; // Check inherent methods: Type::method(receiver, args...) - if self.find_inherent_method(type_or_trait, method_name).is_some() { + if self + .find_inherent_method(type_or_trait, method_name) + .is_some() + { return self.lower_qualified_method_call( type_or_trait, method_name, @@ -108,7 +126,9 @@ impl AstToEgglog { // Check trait impls for non-primitive types: Map::sload(slot), etc. // Directly look up and inline the method from the type's trait impls. - if let Some((fn_decl, body)) = self.find_trait_method_for_type(type_or_trait, method_name) { + if let Some((fn_decl, body)) = + self.find_trait_method_for_type(type_or_trait, method_name) + { let params: Vec<(String, edge_ast::ty::TypeSig)> = fn_decl .params .iter() @@ -236,7 +256,7 @@ impl AstToEgglog { .map(|(id, ty)| (id.name.clone(), ty.clone())) .collect(); // Set type param substitutions for generic method bodies - let old_subst = std::mem::replace(&mut self.type_param_subst, type_param_subst.clone()); + let old_subst = std::mem::replace(&mut self.type_param_subst, type_param_subst); let result = self.inline_function_call(¶ms, &body, &all_args); self.type_param_subst = old_subst; return result; @@ -293,11 +313,8 @@ impl AstToEgglog { if let Some(struct_info) = self.struct_types.get(type_name).cloned() { let recv_ir = self.lower_expr(receiver)?; let base_slot = self.lower_expr(&args[0])?; - let result = self.default_struct_derive_slot( - &recv_ir, - &base_slot, - &struct_info.fields, - ); + let result = + self.default_struct_derive_slot(&recv_ir, &base_slot, &struct_info.fields); return Ok(result); } } @@ -415,7 +432,7 @@ impl AstToEgglog { let receiver_type = self.infer_receiver_type(&args[0]); let is_primitive = receiver_type .as_ref() - .map_or(true, |t| Self::is_primitive_type(t)); + .is_none_or(|t| Self::is_primitive_type(t)); if is_primitive { if let Some(op) = self.compiler_provided_method(method_name) { if args.len() != 2 { @@ -447,7 +464,7 @@ impl AstToEgglog { } // For instance methods like sstore/derive_slot: first arg is receiver if args_ir.len() >= 2 { - let recv = args_ir[0].clone(); + let recv = Rc::clone(&args_ir[0]); if let Some(result) = self.compiler_provided_stateful_method( method_name, Some(recv), @@ -607,7 +624,10 @@ impl AstToEgglog { /// Infer the type of a receiver expression (best-effort). /// Look up the scope binding for an expression (Ident or self.field). /// Returns the variable name and binding reference if found. - fn lookup_binding_for_expr<'a>(&'a self, expr: &edge_ast::Expr) -> Option<&'a super::VarBinding> { + fn lookup_binding_for_expr<'a>( + &'a self, + expr: &edge_ast::Expr, + ) -> Option<&'a super::VarBinding> { let var_name = match expr { edge_ast::Expr::Ident(ident) => &ident.name, edge_ast::Expr::FieldAccess(obj, field, _) => { @@ -644,9 +664,7 @@ impl AstToEgglog { edge_ast::Expr::StructInstantiation(_, type_name, _, _) => Some(type_name.name.clone()), edge_ast::Expr::Literal(lit) => match lit.as_ref() { edge_ast::Lit::Bool(_, _) => Some("bool".to_string()), - edge_ast::Lit::Int(_, Some(pt), _) => { - Some(Self::primitive_type_to_name(pt)) - } + edge_ast::Lit::Int(_, Some(pt), _) => Some(Self::primitive_type_to_name(pt)), edge_ast::Lit::Int(_, None, _) => Some("u256".to_string()), _ => None, }, @@ -667,7 +685,10 @@ impl AstToEgglog { /// Look up the Index trait impl's Output type for a given type name. /// Returns the mangled name of the Output type if the type implements Index. pub(crate) fn index_output_type(&self, type_name: &str) -> Option { - if let Some(impl_info) = self.trait_impls.get(&(type_name.to_string(), "Index".to_string())) { + if let Some(impl_info) = self + .trait_impls + .get(&(type_name.to_string(), "Index".to_string())) + { // Index — Output is the second type arg if impl_info.trait_type_args.len() >= 2 { return Some(Self::type_sig_mangle(&impl_info.trait_type_args[1])); @@ -676,9 +697,12 @@ impl AstToEgglog { None } - /// Look up the Index trait impl's Output TypeSig for a given type name. + /// Look up the Index trait impl's Output `TypeSig` for a given type name. pub(crate) fn index_output_type_sig(&self, type_name: &str) -> Option { - if let Some(impl_info) = self.trait_impls.get(&(type_name.to_string(), "Index".to_string())) { + if let Some(impl_info) = self + .trait_impls + .get(&(type_name.to_string(), "Index".to_string())) + { if impl_info.trait_type_args.len() >= 2 { return Some(impl_info.trait_type_args[1].clone()); } @@ -687,7 +711,10 @@ impl AstToEgglog { } /// Get the concrete type arguments for a receiver's generic composite type. - pub(crate) fn infer_receiver_type_args(&self, expr: &edge_ast::Expr) -> Vec { + pub(crate) fn infer_receiver_type_args( + &self, + expr: &edge_ast::Expr, + ) -> Vec { if let Some(binding) = self.lookup_binding_for_expr(expr) { return binding.composite_type_args.clone(); } @@ -697,10 +724,10 @@ impl AstToEgglog { edge_ast::Expr::ArrayIndex(base, _, _, _) => { let base_type = self.infer_receiver_type(base); if let Some(ref bt) = base_type { - if let Some(output_sig) = self.index_output_type_sig(bt) { - if let edge_ast::ty::TypeSig::Named(_, inner_args) = &output_sig { - return inner_args.clone(); - } + if let Some(edge_ast::ty::TypeSig::Named(_, inner_args)) = + self.index_output_type_sig(bt).as_ref() + { + return inner_args.clone(); } } Vec::new() @@ -726,7 +753,7 @@ impl AstToEgglog { let base = type_name.split("__").next().unwrap_or(type_name); self.generic_type_templates.get(base) }); - if let Some(template) = template { + template.map_or_else(HashMap::new, |template| { template .type_params .iter() @@ -736,12 +763,10 @@ impl AstToEgglog { (param.name.name.clone(), name) }) .collect() - } else { - HashMap::new() - } + }) } - /// Convert an EvmType to a type name string (for primitives). + /// Convert an `EvmType` to a type name string (for primitives). fn evm_type_to_name(ty: &EvmType) -> Option { match ty { EvmType::Base(base) => match base { @@ -758,7 +783,7 @@ impl AstToEgglog { } } - /// Convert a PrimitiveType to a type name string. + /// Convert a `PrimitiveType` to a type name string. fn primitive_type_to_name(pt: &edge_ast::ty::PrimitiveType) -> String { use edge_ast::ty::PrimitiveType; match pt { @@ -776,16 +801,22 @@ impl AstToEgglog { /// Check if a type name refers to a primitive type (not a user-defined composite). pub(crate) fn is_primitive_type(type_name: &str) -> bool { matches!(type_name, "bool" | "address" | "b32" | "bit") - || type_name.strip_prefix('u').and_then(|s| s.parse::().ok()) + || type_name + .strip_prefix('u') + .and_then(|s| s.parse::().ok()) .is_some_and(|w| (8..=256).contains(&w) && w % 8 == 0) - || type_name.strip_prefix('i').and_then(|s| s.parse::().ok()) + || type_name + .strip_prefix('i') + .and_then(|s| s.parse::().ok()) .is_some_and(|w| (8..=256).contains(&w) && w % 8 == 0) - || type_name.strip_prefix("bytes").and_then(|s| s.parse::().ok()) + || type_name + .strip_prefix("bytes") + .and_then(|s| s.parse::().ok()) .is_some_and(|n| (1..=32).contains(&n)) } /// Look up a compiler-provided trait method for a primitive type. - /// Returns the binary op if the method matches an imported std::ops trait. + /// Returns the binary op if the method matches an imported `std::ops` trait. fn compiler_provided_method(&self, method_name: &str) -> Option { match method_name { "unsafe_add" if self.std_ops_traits.contains("UnsafeAdd") => Some(EvmBinaryOp::Add), @@ -831,11 +862,8 @@ impl AstToEgglog { let base_slot = args_ir.first()?; let scratch = self.alloc_region(2); // MSTORE(scratch, key) - let mstore_key = ast_helpers::mstore( - Rc::clone(&scratch), - key, - Rc::clone(&self.current_state), - ); + let mstore_key = + ast_helpers::mstore(Rc::clone(&scratch), key, Rc::clone(&self.current_state)); self.current_state = Rc::clone(&mstore_key); // MSTORE(scratch+32, base_slot) let slot_offset = ast_helpers::add( @@ -865,7 +893,7 @@ impl AstToEgglog { recv } else { // Called as Type::sload(slot) — first arg is the slot - args_ir.first()?.clone() + Rc::clone(args_ir.first()?) }; Some(ast_helpers::sload(slot, Rc::clone(&self.current_state))) } @@ -874,11 +902,8 @@ impl AstToEgglog { "sstore" if self.std_ops_traits.contains("Sstore") => { let value = receiver_ir?; let slot = args_ir.first()?; - let store = ast_helpers::sstore( - Rc::clone(slot), - value, - Rc::clone(&self.current_state), - ); + let store = + ast_helpers::sstore(Rc::clone(slot), value, Rc::clone(&self.current_state)); self.current_state = Rc::clone(&store); Some(store) } @@ -906,10 +931,8 @@ impl AstToEgglog { ) -> RcExpr { let scratch = self.alloc_region(2); let mut current_slot = Rc::clone(base_slot); - let mut side_effects = ast_helpers::empty( - EvmType::Base(EvmBaseType::UnitT), - self.current_ctx.clone(), - ); + let mut side_effects = + ast_helpers::empty(EvmType::Base(EvmBaseType::UnitT), self.current_ctx.clone()); for (i, (_name, _ty)) in fields.iter().enumerate() { // Load field value: MLOAD(receiver + i*32) @@ -933,11 +956,8 @@ impl AstToEgglog { Rc::clone(&scratch), ast_helpers::const_int(32, self.current_ctx.clone()), ); - let mstore_slot = ast_helpers::mstore( - slot_offset, - current_slot, - Rc::clone(&self.current_state), - ); + let mstore_slot = + ast_helpers::mstore(slot_offset, current_slot, Rc::clone(&self.current_state)); self.current_state = Rc::clone(&mstore_slot); side_effects = ast_helpers::concat(side_effects, mstore_slot); @@ -1043,13 +1063,17 @@ impl AstToEgglog { params.iter().map(|(n, _)| n.as_str()).collect::>(), args.len() ); - let mut arg_composite: Vec, Vec)>> = Vec::new(); + #[allow(clippy::type_complexity)] + let mut arg_composite: Vec< + Option<(String, Option, Vec)>, + > = Vec::new(); for arg in args { if let edge_ast::Expr::Ident(ident) = arg { let info = self.lookup_composite_info(&ident.name); if let Some((ct, cb)) = info { // Also grab composite_type_args from the binding - let type_args = self.lookup_binding_for_expr(arg) + let type_args = self + .lookup_binding_for_expr(arg) .map(|b| b.composite_type_args.clone()) .unwrap_or_default(); arg_composite.push(Some((ct, Some(cb), type_args))); @@ -1059,7 +1083,11 @@ impl AstToEgglog { for scope in self.scopes.iter().rev() { if let Some(binding) = scope.bindings.get(&ident.name) { if let Some(ref ct) = binding.composite_type { - arg_composite.push(Some((ct.clone(), None, binding.composite_type_args.clone()))); + arg_composite.push(Some(( + ct.clone(), + None, + binding.composite_type_args.clone(), + ))); found = true; } break; @@ -1076,7 +1104,8 @@ impl AstToEgglog { if let Some(ref bt) = base_type { if let Some(output_sig) = self.index_output_type_sig(bt) { let value_mangled = Self::type_sig_mangle(&output_sig); - let inner_args = if let edge_ast::ty::TypeSig::Named(_, inner) = &output_sig { + let inner_args = if let edge_ast::ty::TypeSig::Named(_, inner) = &output_sig + { inner.clone() } else { Vec::new() @@ -1149,11 +1178,9 @@ impl AstToEgglog { } } else { // Named type with type args — try monomorphizing - if let Ok(Some(mangled)) = self.try_monomorphize_named_type( - &resolved_name, - type_args, - None, - ) { + if let Ok(Some(mangled)) = + self.try_monomorphize_named_type(&resolved_name, type_args, None) + { composite_type = Some(mangled); if composite_type_args.is_empty() { composite_type_args = type_args.clone(); diff --git a/crates/ir/src/to_egglog/composite.rs b/crates/ir/src/to_egglog/composite.rs index c7c4654..a56be4c 100644 --- a/crates/ir/src/to_egglog/composite.rs +++ b/crates/ir/src/to_egglog/composite.rs @@ -2,13 +2,14 @@ use std::rc::Rc; +use edge_diagnostics; + use super::AstToEgglog; use crate::{ ast_helpers, schema::{EvmBaseType, EvmBinaryOp, EvmType, RcExpr}, IrError, }; -use edge_diagnostics; impl AstToEgglog { /// Look up the variant index for a union type. @@ -24,8 +25,9 @@ impl AstToEgglog { v } else if let Some(mangled) = self.resolve_generic_type_name(type_name) { self.union_types.get(&mangled).ok_or_else(|| { - let diag = - edge_diagnostics::Diagnostic::error(format!("unknown union type: `{type_name}`")); + let diag = edge_diagnostics::Diagnostic::error(format!( + "unknown union type: `{type_name}`" + )); IrError::Diagnostic(if let Some(s) = span { diag.with_label(s.clone(), "not found") } else { @@ -34,7 +36,9 @@ impl AstToEgglog { })? } else { // Check if resolution failed due to ambiguity (multiple monomorphizations) - let candidate_count = self.monomorphized_types.iter() + let candidate_count = self + .monomorphized_types + .iter() .filter(|((base, _), _)| base == type_name) .count(); let diag = if candidate_count > 1 { @@ -102,19 +106,15 @@ impl AstToEgglog { }) })? }; - let variants = self - .union_types - .get(&resolved_name) - .ok_or_else(|| { - let diag = edge_diagnostics::Diagnostic::error(format!( - "unknown union type: `{type_name}`", - )); - IrError::Diagnostic(if let Some(s) = span { - diag.with_label(s.clone(), "not found") - } else { - diag - }) - })?; + let variants = self.union_types.get(&resolved_name).ok_or_else(|| { + let diag = + edge_diagnostics::Diagnostic::error(format!("unknown union type: `{type_name}`",)); + IrError::Diagnostic(if let Some(s) = span { + diag.with_label(s.clone(), "not found") + } else { + diag + }) + })?; let has_data = variants.get(idx).map(|(_, d)| *d).unwrap_or(false); if !has_data || args.is_empty() { @@ -163,22 +163,24 @@ impl AstToEgglog { type_name.to_string() } else { // Try precise resolution via type_sig_hint first - let from_hint = if let Some(edge_ast::ty::TypeSig::Named(ref hint_name, ref hint_args)) = self.type_sig_hint { - if (hint_name.name == type_name || hint_name.name.starts_with(type_name)) && !hint_args.is_empty() { - self.resolve_generic_type_name_with_args(type_name, hint_args) + let from_hint = + if let Some(edge_ast::ty::TypeSig::Named(ref hint_name, ref hint_args)) = + self.type_sig_hint + { + if (hint_name.name == type_name || hint_name.name.starts_with(type_name)) + && !hint_args.is_empty() + { + self.resolve_generic_type_name_with_args(type_name, hint_args) + } else { + None + } } else { None - } - } else { - None - }; - if let Some(resolved) = from_hint { - resolved - } else { - // Fall back to unambiguous resolution + }; + from_hint.unwrap_or_else(|| { self.resolve_generic_type_name(type_name) .unwrap_or_else(|| type_name.to_string()) - } + }) }; let struct_info = self.struct_types.get(&resolved_name).cloned(); diff --git a/crates/ir/src/to_egglog/control_flow.rs b/crates/ir/src/to_egglog/control_flow.rs index dc861ef..af8832a 100644 --- a/crates/ir/src/to_egglog/control_flow.rs +++ b/crates/ir/src/to_egglog/control_flow.rs @@ -66,7 +66,7 @@ impl AstToEgglog { let_bind_name: Some(var_name), composite_type: None, composite_base: None, - composite_type_args: Vec::new(), + composite_type_args: Vec::new(), }, ); } @@ -177,7 +177,7 @@ impl AstToEgglog { let_bind_name: Some(var_name.clone()), composite_type: None, composite_base: None, - composite_type_args: Vec::new(), + composite_type_args: Vec::new(), }; self.scopes .last_mut() diff --git a/crates/ir/src/to_egglog/expr.rs b/crates/ir/src/to_egglog/expr.rs index 5087983..d7a7d00 100644 --- a/crates/ir/src/to_egglog/expr.rs +++ b/crates/ir/src/to_egglog/expr.rs @@ -201,10 +201,14 @@ impl AstToEgglog { // Intercept ArrayIndex write for Index/Map dispatch: base[index] = val → base.set(index, val) if let edge_ast::Expr::ArrayIndex(arr_base, arr_index, _, arr_span) = lhs { - if let Some(result) = self.try_lower_storage_array_write(arr_base, arr_index, &rhs_ir)? { + if let Some(result) = + self.try_lower_storage_array_write(arr_base, arr_index, &rhs_ir)? + { return Ok(result); } - if let Some(result) = self.try_lower_array_element_write(arr_base, arr_index, &rhs_ir)? { + if let Some(result) = + self.try_lower_array_element_write(arr_base, arr_index, &rhs_ir)? + { return Ok(result); } if self.std_ops_traits.contains("Index") { @@ -468,10 +472,14 @@ impl AstToEgglog { // Intercept ArrayIndex write for Index/Map dispatch: base[index] = val → base.set(index, val) if let edge_ast::Expr::ArrayIndex(arr_base, arr_index, _, arr_span) = lhs.as_ref() { - if let Some(result) = self.try_lower_storage_array_write(arr_base, arr_index, &rhs_ir)? { + if let Some(result) = + self.try_lower_storage_array_write(arr_base, arr_index, &rhs_ir)? + { return Ok(result); } - if let Some(result) = self.try_lower_array_element_write(arr_base, arr_index, &rhs_ir)? { + if let Some(result) = + self.try_lower_array_element_write(arr_base, arr_index, &rhs_ir)? + { return Ok(result); } if self.std_ops_traits.contains("Index") { @@ -549,12 +557,7 @@ impl AstToEgglog { // Try Index trait dispatch: base[index] → base.index(index) if self.std_ops_traits.contains("Index") { - return self.lower_method_call( - base, - "index", - &[index.as_ref().clone()], - _span, - ); + return self.lower_method_call(base, "index", &[index.as_ref().clone()], _span); } Err(IrError::Unsupported( @@ -574,7 +577,12 @@ impl AstToEgglog { let type_name = &components[0].name; let variant_name = &components[1].name; if self.union_types.contains_key(type_name) { - return self.lower_union_instantiation_expr(type_name, variant_name, &[], Some(span)); + return self.lower_union_instantiation_expr( + type_name, + variant_name, + &[], + Some(span), + ); } // Check for generic union types (e.g., Option::None where Option was monomorphized) if self.generic_type_templates.contains_key(type_name) { @@ -646,9 +654,13 @@ impl AstToEgglog { self.lower_array_instantiation(elements) } - edge_ast::Expr::UnionInstantiation(type_name, variant_name, args, span) => { - self.lower_union_instantiation_expr(&type_name.name, &variant_name.name, args, Some(span)) - } + edge_ast::Expr::UnionInstantiation(type_name, variant_name, args, span) => self + .lower_union_instantiation_expr( + &type_name.name, + &variant_name.name, + args, + Some(span), + ), edge_ast::Expr::PatternMatch(expr, pattern, _span) => { self.lower_pattern_match(expr, pattern) @@ -744,8 +756,7 @@ impl AstToEgglog { // Note: () can lower as either Base(UnitT) or TupleT([]). let is_unit = matches!(binding._ty, EvmType::Base(EvmBaseType::UnitT)) || matches!(&binding._ty, EvmType::TupleT(v) if v.is_empty()); - if binding.storage_slot.is_some() && is_unit - { + if binding.storage_slot.is_some() && is_unit { return Ok(ast_helpers::const_int( binding.storage_slot.unwrap_or(0) as i64, self.current_ctx.clone(), @@ -897,7 +908,8 @@ impl AstToEgglog { } // Index write dispatch is handled in the Assign branch above Err(IrError::Unsupported( - "array index write on non-array type; use Map.set(key, val) for mappings".to_owned(), + "array index write on non-array type; use Map.set(key, val) for mappings" + .to_owned(), )) } edge_ast::Expr::FieldAccess(obj, field, _span) => { diff --git a/crates/ir/src/to_egglog/function.rs b/crates/ir/src/to_egglog/function.rs index a12ff25..3dbc509 100644 --- a/crates/ir/src/to_egglog/function.rs +++ b/crates/ir/src/to_egglog/function.rs @@ -171,8 +171,7 @@ impl AstToEgglog { Rc::clone(&self.current_state), ); self.current_state = Rc::clone(&mstore_expr); - let ret = - ast_helpers::return_op(ret_buf, size, Rc::clone(&self.current_state)); + let ret = ast_helpers::return_op(ret_buf, size, Rc::clone(&self.current_state)); Ok(ast_helpers::concat(mstore_expr, ret)) } else { // No return type, or body already has explicit return. @@ -570,7 +569,7 @@ impl AstToEgglog { let_bind_name: None, composite_type: None, composite_base: None, - composite_type_args: Vec::new(), + composite_type_args: Vec::new(), }; self.scopes .last_mut() diff --git a/crates/ir/src/to_egglog/mod.rs b/crates/ir/src/to_egglog/mod.rs index 92c97ee..291f1e5 100644 --- a/crates/ir/src/to_egglog/mod.rs +++ b/crates/ir/src/to_egglog/mod.rs @@ -14,7 +14,10 @@ mod pattern; mod storage; mod types; -use std::{collections::{HashMap, HashSet}, rc::Rc}; +use std::{ + collections::{HashMap, HashSet}, + rc::Rc, +}; use indexmap::IndexMap; @@ -314,7 +317,7 @@ pub struct AstToEgglog { // ---- Generics & Traits ---- /// Generic type templates: name -> template info (type params + original `TypeSig`) pub(crate) generic_type_templates: IndexMap, - /// Generic impl blocks: base_type_name -> list of impl blocks (for monomorphization) + /// Generic impl blocks: `base_type_name` -> list of impl blocks (for monomorphization) pub(crate) generic_impl_blocks: IndexMap>, /// Cache of monomorphized types: (`generic_name`, `concrete_types`) -> `mangled_name` pub(crate) monomorphized_types: IndexMap<(String, Vec), String>, @@ -336,7 +339,7 @@ pub struct AstToEgglog { /// Type hint from assignment target, used for generic return-type inference. /// Set before lowering the RHS of a typed variable assignment, cleared after. pub(crate) type_hint: Option, - /// TypeSig hint from assignment target, used to disambiguate generic struct instantiation. + /// `TypeSig` hint from assignment target, used to disambiguate generic struct instantiation. /// Set before lowering the RHS of a typed variable declaration, cleared after. pub(crate) type_sig_hint: Option, /// Compiler warnings collected during lowering @@ -404,8 +407,10 @@ impl AstToEgglog { } /// Extract the type name and type args from a Named type sig, unwrapping Pointer wrappers. - /// Returns (base_name, type_args), e.g., ("Map", [addr, u256]) from `&s Map`. - fn extract_named_type(type_sig: &edge_ast::ty::TypeSig) -> Option<(String, Vec)> { + /// Returns (`base_name`, `type_args`), e.g., ("Map", [addr, u256]) from `&s Map`. + fn extract_named_type( + type_sig: &edge_ast::ty::TypeSig, + ) -> Option<(String, Vec)> { match type_sig { edge_ast::ty::TypeSig::Named(name, args) => Some((name.name.clone(), args.clone())), edge_ast::ty::TypeSig::Pointer(_, inner) => Self::extract_named_type(inner), @@ -504,14 +509,12 @@ impl AstToEgglog { // `Map` which requires `addr: UniqueSlot` and `u256: Sload & Sstore`. { let primitive_types = [ - "u256", "u248", "u240", "u232", "u224", "u216", "u208", "u200", - "u192", "u184", "u176", "u168", "u160", "u152", "u144", "u136", - "u128", "u120", "u112", "u104", "u96", "u88", "u80", "u72", - "u64", "u56", "u48", "u40", "u32", "u24", "u16", "u8", - "i256", "i248", "i240", "i232", "i224", "i216", "i208", "i200", - "i192", "i184", "i176", "i168", "i160", "i152", "i144", "i136", - "i128", "i120", "i112", "i104", "i96", "i88", "i80", "i72", - "i64", "i56", "i48", "i40", "i32", "i24", "i16", "i8", + "u256", "u248", "u240", "u232", "u224", "u216", "u208", "u200", "u192", "u184", + "u176", "u168", "u160", "u152", "u144", "u136", "u128", "u120", "u112", "u104", + "u96", "u88", "u80", "u72", "u64", "u56", "u48", "u40", "u32", "u24", "u16", "u8", + "i256", "i248", "i240", "i232", "i224", "i216", "i208", "i200", "i192", "i184", + "i176", "i168", "i160", "i152", "i144", "i136", "i128", "i120", "i112", "i104", + "i96", "i88", "i80", "i72", "i64", "i56", "i48", "i40", "i32", "i24", "i16", "i8", "address", "bool", "b32", ]; let primitive_traits = ["UniqueSlot", "Sload", "Sstore"]; @@ -697,8 +700,11 @@ impl AstToEgglog { if !impl_block.type_params.is_empty() || self.generic_type_templates.contains_key(&type_name) { - let trait_name = impl_block.trait_impl.as_ref().map(|(n, _)| n.name.clone()); - let trait_type_params = impl_block.trait_impl.as_ref() + let trait_name = + impl_block.trait_impl.as_ref().map(|(n, _)| n.name.clone()); + let trait_type_params = impl_block + .trait_impl + .as_ref() .map(|(_, params)| params.clone()) .unwrap_or_default(); self.generic_impl_blocks @@ -767,11 +773,15 @@ impl AstToEgglog { } // Extract trait type args from the impl declaration - let trait_type_args: Vec = impl_block.trait_impl + let trait_type_args: Vec = impl_block + .trait_impl .as_ref() .map(|(_, params)| { - params.iter() - .map(|p| edge_ast::ty::TypeSig::Named(p.name.clone(), Vec::new())) + params + .iter() + .map(|p| { + edge_ast::ty::TypeSig::Named(p.name.clone(), Vec::new()) + }) .collect() }) .unwrap_or_default(); @@ -1017,7 +1027,7 @@ impl AstToEgglog { let_bind_name: None, composite_type: None, composite_base: None, - composite_type_args: Vec::new(), + composite_type_args: Vec::new(), }; self.scopes .last_mut() diff --git a/crates/ir/src/to_egglog/pattern.rs b/crates/ir/src/to_egglog/pattern.rs index 9b14cac..b7c2546 100644 --- a/crates/ir/src/to_egglog/pattern.rs +++ b/crates/ir/src/to_egglog/pattern.rs @@ -17,7 +17,11 @@ impl AstToEgglog { pattern: &edge_ast::pattern::UnionPattern, ) -> Result { let disc_ir = self.lower_expr(expr)?; - let idx = self.variant_index(&pattern.union_name.name, &pattern.member_name.name, Some(&pattern.span))?; + let idx = self.variant_index( + &pattern.union_name.name, + &pattern.member_name.name, + Some(&pattern.span), + )?; let idx_ir = ast_helpers::const_int(idx as i64, self.current_ctx.clone()); Ok(ast_helpers::eq(disc_ir, idx_ir)) } @@ -73,7 +77,11 @@ impl AstToEgglog { for arm in arms { match &arm.pattern { edge_ast::pattern::MatchPattern::Union(up) => { - let idx = self.variant_index(&up.union_name.name, &up.member_name.name, Some(&up.span))?; + let idx = self.variant_index( + &up.union_name.name, + &up.member_name.name, + Some(&up.span), + )?; let bindings: Vec = up.bindings.iter().map(|b| b.name.clone()).collect(); variant_arms.push((idx, &arm.body, bindings)); @@ -126,7 +134,7 @@ impl AstToEgglog { let_bind_name: Some(var_name), composite_type: None, composite_base: None, - composite_type_args: Vec::new(), + composite_type_args: Vec::new(), }, ); } @@ -177,7 +185,11 @@ impl AstToEgglog { Rc::clone(&disc_ir) }; - let idx = self.variant_index(&pattern.union_name.name, &pattern.member_name.name, Some(&pattern.span))?; + let idx = self.variant_index( + &pattern.union_name.name, + &pattern.member_name.name, + Some(&pattern.span), + )?; let idx_ir = ast_helpers::const_int(idx as i64, self.current_ctx.clone()); let cond = ast_helpers::eq(disc_val, idx_ir); let inputs = @@ -205,7 +217,7 @@ impl AstToEgglog { let_bind_name: Some(var_name), composite_type: None, composite_base: None, - composite_type_args: Vec::new(), + composite_type_args: Vec::new(), }, ); } diff --git a/crates/ir/src/to_egglog/storage.rs b/crates/ir/src/to_egglog/storage.rs index c350fcf..a4d2304 100644 --- a/crates/ir/src/to_egglog/storage.rs +++ b/crates/ir/src/to_egglog/storage.rs @@ -2,13 +2,14 @@ use std::rc::Rc; +use edge_diagnostics; + use super::AstToEgglog; use crate::{ ast_helpers, schema::{DataLocation, EvmExpr, RcExpr}, IrError, }; -use edge_diagnostics; impl AstToEgglog { /// Lower an emit statement. @@ -117,6 +118,7 @@ impl AstToEgglog { } /// Find the storage slot index and data location for a named field. + #[allow(dead_code)] pub(crate) fn find_storage_slot(&self, name: &str) -> Result<(usize, DataLocation), IrError> { for scope in self.scopes.iter().rev() { if let Some(binding) = scope.bindings.get(name) { @@ -125,10 +127,8 @@ impl AstToEgglog { } } } - Err(IrError::Diagnostic( - edge_diagnostics::Diagnostic::error(format!( - "cannot find storage field `{name}` in the current contract", - )), - )) + Err(IrError::Diagnostic(edge_diagnostics::Diagnostic::error( + format!("cannot find storage field `{name}` in the current contract",), + ))) } } diff --git a/crates/ir/src/to_egglog/types.rs b/crates/ir/src/to_egglog/types.rs index 3ec7ee2..0f2d8ea 100644 --- a/crates/ir/src/to_egglog/types.rs +++ b/crates/ir/src/to_egglog/types.rs @@ -3,6 +3,7 @@ use std::collections::{HashMap, HashSet}; use indexmap::IndexMap; + use super::{AstToEgglog, StructTypeInfo}; use crate::{ schema::{DataLocation, EvmBaseType, EvmType}, @@ -46,7 +47,9 @@ impl AstToEgglog { /// `resolve_generic_type_name_with_args` for precise resolution. pub(crate) fn resolve_generic_type_name(&self, name: &str) -> Option { // Check monomorphized_types cache for entries with this base name - let candidates: Vec<&String> = self.monomorphized_types.iter() + let candidates: Vec<&String> = self + .monomorphized_types + .iter() .filter(|((base, _), _)| base == name) .map(|(_, mangled)| mangled) .collect(); @@ -85,9 +88,7 @@ impl AstToEgglog { name: &str, type_args: &[edge_ast::ty::TypeSig], ) -> Option { - let mangled_args: Vec = type_args.iter() - .map(|a| Self::type_sig_mangle(a)) - .collect(); + let mangled_args: Vec = type_args.iter().map(Self::type_sig_mangle).collect(); let cache_key = (name.to_string(), mangled_args); if let Some(mangled) = self.monomorphized_types.get(&cache_key) { return Some(mangled.clone()); @@ -329,22 +330,25 @@ impl AstToEgglog { } /// Substitute type parameters in a code block (AST-level). - /// Replaces type param names in Path expressions (e.g., V::sload → u256::sload). - /// For generic types like Map, uses the mangled name (Map__address_u256) + /// Replaces type param names in Path expressions (e.g., `V::sload` → `u256::sload`). + /// For generic types like Map, uses the mangled name (`Map__address_u256`) /// so that qualified calls resolve to monomorphized trait impls. fn substitute_code_block( block: &edge_ast::CodeBlock, subst: &HashMap, ) -> edge_ast::CodeBlock { // Build a string→string map for path substitution using mangled names - let name_subst: HashMap<&str, String> = subst.iter().map(|(k, v)| { - (k.as_str(), Self::type_sig_mangle(v)) - }).collect(); + let name_subst: HashMap<&str, String> = subst + .iter() + .map(|(k, v)| (k.as_str(), Self::type_sig_mangle(v))) + .collect(); edge_ast::CodeBlock { - stmts: block.stmts.iter().map(|item| { - Self::substitute_block_item(item, &name_subst) - }).collect(), + stmts: block + .stmts + .iter() + .map(|item| Self::substitute_block_item(item, &name_subst)) + .collect(), span: block.span.clone(), } } @@ -363,79 +367,68 @@ impl AstToEgglog { } } - fn substitute_stmt( - stmt: &edge_ast::Stmt, - subst: &HashMap<&str, String>, - ) -> edge_ast::Stmt { + fn substitute_stmt(stmt: &edge_ast::Stmt, subst: &HashMap<&str, String>) -> edge_ast::Stmt { match stmt { - edge_ast::Stmt::VarDecl(ident, ty, init, span) => { - edge_ast::Stmt::VarDecl( - ident.clone(), - ty.clone(), - init.as_ref().map(|e| Box::new(Self::substitute_expr(e, subst))), - span.clone(), - ) - } - edge_ast::Stmt::VarAssign(lhs, rhs, span) => { - edge_ast::Stmt::VarAssign( - Self::substitute_expr(lhs, subst), - Self::substitute_expr(rhs, subst), - span.clone(), - ) - } + edge_ast::Stmt::VarDecl(ident, ty, init, span) => edge_ast::Stmt::VarDecl( + ident.clone(), + ty.clone(), + init.as_ref() + .map(|e| Box::new(Self::substitute_expr(e, subst))), + span.clone(), + ), + edge_ast::Stmt::VarAssign(lhs, rhs, span) => edge_ast::Stmt::VarAssign( + Self::substitute_expr(lhs, subst), + Self::substitute_expr(rhs, subst), + span.clone(), + ), edge_ast::Stmt::Return(Some(expr), span) => { edge_ast::Stmt::Return(Some(Self::substitute_expr(expr, subst)), span.clone()) } - edge_ast::Stmt::Expr(expr) => { - edge_ast::Stmt::Expr(Self::substitute_expr(expr, subst)) - } + edge_ast::Stmt::Expr(expr) => edge_ast::Stmt::Expr(Self::substitute_expr(expr, subst)), other => other.clone(), } } - fn substitute_expr( - expr: &edge_ast::Expr, - subst: &HashMap<&str, String>, - ) -> edge_ast::Expr { + fn substitute_expr(expr: &edge_ast::Expr, subst: &HashMap<&str, String>) -> edge_ast::Expr { match expr { edge_ast::Expr::Path(components, span) => { - let new_components: Vec = components.iter().map(|c| { - if let Some(replacement) = subst.get(c.name.as_str()) { - edge_ast::Ident { name: replacement.clone(), span: c.span.clone() } - } else { - c.clone() - } - }).collect(); + let new_components: Vec = components + .iter() + .map(|c| { + subst.get(c.name.as_str()).map_or_else( + || c.clone(), + |replacement| edge_ast::Ident { + name: replacement.clone(), + span: c.span.clone(), + }, + ) + }) + .collect(); edge_ast::Expr::Path(new_components, span.clone()) } edge_ast::Expr::FunctionCall(callee, args, turbofish, span) => { edge_ast::Expr::FunctionCall( Box::new(Self::substitute_expr(callee, subst)), - args.iter().map(|a| Self::substitute_expr(a, subst)).collect(), + args.iter() + .map(|a| Self::substitute_expr(a, subst)) + .collect(), turbofish.clone(), span.clone(), ) } - edge_ast::Expr::FieldAccess(obj, field, span) => { - edge_ast::Expr::FieldAccess( - Box::new(Self::substitute_expr(obj, subst)), - field.clone(), - span.clone(), - ) - } - edge_ast::Expr::Binary(lhs, op, rhs, span) => { - edge_ast::Expr::Binary( - Box::new(Self::substitute_expr(lhs, subst)), - op.clone(), - Box::new(Self::substitute_expr(rhs, subst)), - span.clone(), - ) - } + edge_ast::Expr::FieldAccess(obj, field, span) => edge_ast::Expr::FieldAccess( + Box::new(Self::substitute_expr(obj, subst)), + field.clone(), + span.clone(), + ), + edge_ast::Expr::Binary(lhs, op, rhs, span) => edge_ast::Expr::Binary( + Box::new(Self::substitute_expr(lhs, subst)), + *op, + Box::new(Self::substitute_expr(rhs, subst)), + span.clone(), + ), edge_ast::Expr::Paren(inner, span) => { - edge_ast::Expr::Paren( - Box::new(Self::substitute_expr(inner, subst)), - span.clone(), - ) + edge_ast::Expr::Paren(Box::new(Self::substitute_expr(inner, subst)), span.clone()) } _ => expr.clone(), } @@ -512,8 +505,7 @@ impl AstToEgglog { ) -> Result { // Use mangled type names for caching — EvmType loses source-level // distinctions (e.g., CustomHash and u256 both lower to UIntT(256)). - let cache_key_types: Vec = - type_args.iter().map(Self::type_sig_mangle).collect(); + let cache_key_types: Vec = type_args.iter().map(Self::type_sig_mangle).collect(); // Check cache let cache_key = (generic_name.to_string(), cache_key_types); @@ -582,9 +574,13 @@ impl AstToEgglog { }; for constraint in &tp.constraints { let key = (concrete_name.clone(), constraint.name.clone()); - let mangled_key = mangled_name.as_ref().map(|m| (m.clone(), constraint.name.clone())); + let mangled_key = mangled_name + .as_ref() + .map(|m| (m.clone(), constraint.name.clone())); let satisfied = self.trait_impls.contains_key(&key) - || mangled_key.as_ref().map_or(false, |k| self.trait_impls.contains_key(k)); + || mangled_key + .as_ref() + .is_some_and(|k| self.trait_impls.contains_key(k)); if !satisfied { let mut diag = edge_diagnostics::Diagnostic::error(format!( "the trait bound `{}: {}` is not satisfied", @@ -658,54 +654,73 @@ impl AstToEgglog { if let Some(impl_blocks) = self.generic_impl_blocks.get(generic_name).cloned() { for gib in &impl_blocks { // Build substitution from the generic impl's type params to concrete args - let impl_subst: HashMap = if gib.type_params.is_empty() { - // Use the type template's params (e.g., `impl Map` where K,V from the type) - subst.clone() - } else { - gib.type_params.iter() - .zip(type_args.iter()) - .map(|(param, arg)| (param.name.name.clone(), arg.clone())) - .collect() - }; + let impl_subst: HashMap = + if gib.type_params.is_empty() { + // Use the type template's params (e.g., `impl Map` where K,V from the type) + subst.clone() + } else { + gib.type_params + .iter() + .zip(type_args.iter()) + .map(|(param, arg)| (param.name.name.clone(), arg.clone())) + .collect() + }; // Substitute type params in method bodies and register under mangled name - let concrete_methods: Vec = gib.items.iter().map(|item| { - match item { - edge_ast::item::ImplItem::FnAssign(fn_decl, body) => { - let new_params: Vec<(edge_ast::Ident, edge_ast::ty::TypeSig)> = fn_decl.params.iter().map(|(id, ty)| { - (id.clone(), Self::substitute_type_params(ty, &impl_subst)) - }).collect(); - let new_returns: Vec = fn_decl.returns.iter().map(|ty| { - Self::substitute_type_params(ty, &impl_subst) - }).collect(); - let new_fn_decl = edge_ast::item::FnDecl { - name: fn_decl.name.clone(), - params: new_params, - returns: new_returns, - type_params: Vec::new(), // concrete, no type params - is_pub: fn_decl.is_pub, - is_ext: fn_decl.is_ext, - is_mut: fn_decl.is_mut, - span: fn_decl.span.clone(), - }; - // Substitute type params in body expressions - let new_body = Self::substitute_code_block(body, &impl_subst); - edge_ast::item::ImplItem::FnAssign(new_fn_decl, new_body) + let concrete_methods: Vec = gib + .items + .iter() + .map(|item| { + match item { + edge_ast::item::ImplItem::FnAssign(fn_decl, body) => { + let new_params: Vec<(edge_ast::Ident, edge_ast::ty::TypeSig)> = + fn_decl + .params + .iter() + .map(|(id, ty)| { + ( + id.clone(), + Self::substitute_type_params(ty, &impl_subst), + ) + }) + .collect(); + let new_returns: Vec = fn_decl + .returns + .iter() + .map(|ty| Self::substitute_type_params(ty, &impl_subst)) + .collect(); + let new_fn_decl = edge_ast::item::FnDecl { + name: fn_decl.name.clone(), + params: new_params, + returns: new_returns, + type_params: Vec::new(), // concrete, no type params + is_pub: fn_decl.is_pub, + is_ext: fn_decl.is_ext, + is_mut: fn_decl.is_mut, + span: fn_decl.span.clone(), + }; + // Substitute type params in body expressions + let new_body = Self::substitute_code_block(body, &impl_subst); + edge_ast::item::ImplItem::FnAssign(new_fn_decl, new_body) + } + other => other.clone(), } - other => other.clone(), - } - }).collect(); + }) + .collect(); if let Some(ref trait_name) = gib.trait_impl { // Trait impl: register under mangled type name let mut methods = IndexMap::new(); for item in &concrete_methods { if let edge_ast::item::ImplItem::FnAssign(fn_decl, body) = item { - methods.insert(fn_decl.name.name.clone(), (fn_decl.clone(), body.clone())); + methods + .insert(fn_decl.name.name.clone(), (fn_decl.clone(), body.clone())); } } // Substitute type params in trait type args to get concrete types - let trait_type_args: Vec = gib.trait_type_params.iter() + let trait_type_args: Vec = gib + .trait_type_params + .iter() .map(|p| { let sig = edge_ast::ty::TypeSig::Named(p.name.clone(), Vec::new()); Self::substitute_type_params(&sig, &impl_subst) @@ -721,17 +736,23 @@ impl AstToEgglog { ); } else { // Inherent impl: register methods under mangled type name - let methods: Vec = concrete_methods.iter().filter_map(|item| { - if let edge_ast::item::ImplItem::FnAssign(fn_decl, body) = item { - Some(super::InherentMethod { - fn_decl: fn_decl.clone(), - body: body.clone(), - }) - } else { - None - } - }).collect(); - self.inherent_methods.entry(mangled.clone()).or_default().extend(methods); + let methods: Vec = concrete_methods + .iter() + .filter_map(|item| { + if let edge_ast::item::ImplItem::FnAssign(fn_decl, body) = item { + Some(super::InherentMethod { + fn_decl: fn_decl.clone(), + body: body.clone(), + }) + } else { + None + } + }) + .collect(); + self.inherent_methods + .entry(mangled.clone()) + .or_default() + .extend(methods); } } } @@ -849,9 +870,7 @@ impl AstToEgglog { if args.is_empty() { ident.name.clone() } else { - let arg_strs: Vec = args.iter() - .map(Self::type_sig_mangle) - .collect(); + let arg_strs: Vec = args.iter().map(Self::type_sig_mangle).collect(); format!("{}__{}", ident.name, arg_strs.join("_")) } } diff --git a/crates/parser/src/parser.rs b/crates/parser/src/parser.rs index 78248f4..e1ba434 100644 --- a/crates/parser/src/parser.rs +++ b/crates/parser/src/parser.rs @@ -257,7 +257,8 @@ impl Parser { /// Check if the token after the current one is `::` (without advancing). fn lookahead_double_colon(&self) -> bool { - self.cursor + 1 < self.tokens.len() && self.tokens[self.cursor + 1].kind == TokenKind::DoubleColon + self.cursor + 1 < self.tokens.len() + && self.tokens[self.cursor + 1].kind == TokenKind::DoubleColon } /// Get a zero-width span at the end of the previous token.