Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions crates/e2e/tests/suites/examples.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,19 @@ fn test_traits_contract_compiles() {
assert!(!bc.is_empty(), "test_traits.edge produced empty bytecode");
}

// =============================================================================
// regression tests
// =============================================================================

#[test]
fn test_name_collision_compiles() {
let bc = compile_contract("examples/tests/test_name_collision.edge");
assert!(
!bc.is_empty(),
"test_name_collision.edge produced empty bytecode"
);
}

// =============================================================================
// utils/
// =============================================================================
Expand Down
23 changes: 13 additions & 10 deletions crates/ir/src/to_egglog/calls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,24 +110,27 @@ impl AstToEgglog {
return self.inline_function_call(&info.params, &info.body, args);
}

// Check contract functions — emit Call (not inline)
if let Some((name, params, _body)) = self
.contract_functions
// Check non-comptime free functions — emit Call (not inline).
// Free functions are checked before contract functions so that a
// contract function calling a same-named free function resolves to
// the free function (not to itself, which would infinite-recurse).
if let Some(info) = self
.free_fn_bodies
.iter()
.find(|(name, _, _)| *name == fn_name)
.find(|f| f.name == fn_name && !f.is_comptime)
.cloned()
{
return self.emit_call(&name, &params, args);
return self.emit_call(&info.name, &info.params, args);
}

// Check non-comptime free functions — emit Call (not inline)
if let Some(info) = self
.free_fn_bodies
// Check contract functions — emit Call (not inline)
if let Some((name, params, _body)) = self
.contract_functions
.iter()
.find(|f| f.name == fn_name && !f.is_comptime)
.find(|(name, _, _)| *name == fn_name)
.cloned()
{
return self.emit_call(&info.name, &info.params, args);
return self.emit_call(&name, &params, args);
}

// Check generic function templates
Expand Down
52 changes: 34 additions & 18 deletions crates/ir/src/to_egglog/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,7 @@ impl AstToEgglog {

/// Lower an internal function body once as a Function node.
/// Uses `inline_depth` > 0 so `return` produces just the value (not `ReturnOp`).
/// Parameters are bound via Arg/Get so the body works as a standalone subroutine.
/// Parameters are memory-backed (LetBind) so reassignment emits VarStore.
pub(crate) fn lower_internal_function_body(
&mut self,
name: &str,
Expand Down Expand Up @@ -467,25 +467,31 @@ impl AstToEgglog {
);
let out_ty = EvmType::Base(EvmBaseType::UIntT(256)); // TODO: derive from return type

// Bind parameters via Arg/Get
// Bind parameters as memory-backed variables so that reassignment
// (e.g. `x = x / 2`) emits VarStore (pushes nothing) instead of
// replacing the binding value (which pushes a value onto the stack
// and causes stack depth mismatches between if-branches).
self.scopes.push(Scope::new());
let arg_expr = Rc::new(EvmExpr::Arg(in_ty.clone(), self.current_ctx.clone()));
let mut param_var_names = Vec::new();
for (i, (param_name, param_ty)) in params.iter().enumerate() {
let ty = self.lower_type_sig(param_ty);
let param_val = if params.len() == 1 {
Rc::clone(&arg_expr)
} else {
ast_helpers::get(Rc::clone(&arg_expr), i)
};
let var_name = format!("{name}__param_{param_name}");
let binding = VarBinding {
value: param_val,
location: DataLocation::Stack,
value: ast_helpers::var(var_name.clone()),
location: DataLocation::Memory,
storage_slot: None,
_ty: ty,
let_bind_name: None,
let_bind_name: Some(var_name.clone()),
composite_type: None,
composite_base: None,
};
let init = if params.len() == 1 {
Rc::clone(&arg_expr)
} else {
ast_helpers::get(Rc::clone(&arg_expr), i)
};
param_var_names.push((var_name, init));
self.scopes
.last_mut()
.expect("scope stack empty")
Expand All @@ -495,23 +501,33 @@ impl AstToEgglog {

// Lower body with inline_depth > 0 so return produces value, not ReturnOp
self.inline_depth += 1;
// Find the body from contract_functions or free_fn_bodies
// Find the body — prefer free_fn_bodies over contract_functions.
// If both contain a function with the same name, the free function
// is the intended callee (the contract function is the *caller*
// whose body we're currently lowering, so picking it would recurse).
let body = self
.contract_functions
.free_fn_bodies
.iter()
.find(|(n, _, _)| n == name)
.map(|(_, _, b)| b.clone())
.find(|f| f.name == name)
.map(|f| f.body.clone())
.or_else(|| {
self.free_fn_bodies
self.contract_functions
.iter()
.find(|f| f.name == name)
.map(|f| f.body.clone())
.find(|(n, _, _)| n == name)
.map(|(_, _, b)| b.clone())
})
.ok_or_else(|| IrError::Unsupported(format!("internal function not found: {name}")))?;
let body_ir = self.lower_code_block(&body)?;
let mut body_ir = self.lower_code_block(&body)?;
self.inline_depth -= 1;
self.scopes.pop();

// Wrap body in LetBind nodes for each parameter (innermost first).
// inline_depth was > 0 during lowering, so skip Drop (same as
// lower_code_block's pattern for inlined locals).
for (var_name, init) in param_var_names.into_iter().rev() {
body_ir = ast_helpers::let_bind(var_name, init, body_ir);
}

self.current_ctx = saved_ctx;
self.current_state = saved_state;

Expand Down
12 changes: 12 additions & 0 deletions examples/tests/test_name_collision.edge
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Regression test: contract function and free function share the same name.
// This should produce a compile error about the name conflict.

fn helper(x: u256) -> (u256) {
return x + 1;
}

contract NameCollision {
pub fn helper(a: u256) -> (u256) {
return helper(a);
}
}
Loading