Skip to content

Commit b3b7c4a

Browse files
committed
Provide unique name from closures with same name
In YUL it's possible to declare closure with a same name, but in different scope, which previously resulted to func name conflict during LLVM IR construction. To solve this, track YUL scope when building LLVM IR and a special counter that increments every time function with the same name exists in LLVM module. There's still assert if somehow within the same scope there 2 functions with the same name, which should be forbidden by Yul Fixes: #474
1 parent 5aebcc7 commit b3b7c4a

File tree

8 files changed

+213
-19
lines changed

8 files changed

+213
-19
lines changed

crates/llvm-context/src/polkavm/context/mod.rs

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! The LLVM IR generator context.
22
33
use std::cell::RefCell;
4+
use std::collections::BTreeMap;
45
use std::collections::HashMap;
56
use std::rc::Rc;
67

@@ -84,6 +85,10 @@ pub struct Context<'ctx> {
8485
current_function: Option<Rc<RefCell<Function<'ctx>>>>,
8586
/// The loop context stack.
8687
loop_stack: Vec<Loop<'ctx>>,
88+
/// Monotonic counter for unique frontend function name mangling.
89+
function_counter: usize,
90+
/// Scope stack mapping Yul names to mangled LLVM names.
91+
function_scope: Vec<BTreeMap<String, String>>,
8792
/// The PVM memory configuration.
8893
memory_config: SolcStandardJsonInputSettingsPolkaVMMemory,
8994

@@ -246,6 +251,8 @@ impl<'ctx> Context<'ctx> {
246251
functions: HashMap::with_capacity(Self::FUNCTIONS_HASHMAP_INITIAL_CAPACITY),
247252
current_function: None,
248253
loop_stack: Vec::with_capacity(Self::LOOP_STACK_INITIAL_CAPACITY),
254+
function_counter: 0,
255+
function_scope: Vec::new(),
249256
memory_config,
250257

251258
debug_info,
@@ -456,12 +463,23 @@ impl<'ctx> Context<'ctx> {
456463
location: Option<(u32, u32)>,
457464
is_frontend: bool,
458465
) -> anyhow::Result<Rc<RefCell<Function<'ctx>>>> {
459-
assert!(
460-
self.get_function(name, is_frontend).is_none(),
461-
"ICE: function '{name}' declared subsequentally"
462-
);
463-
464-
let name = self.internal_function_name(name, is_frontend);
466+
let name = if is_frontend {
467+
if let Some(scope) = self.function_scope.last() {
468+
assert!(
469+
!scope.contains_key(name),
470+
"ICE: function '{name}' declared subsequently in the same scope"
471+
);
472+
}
473+
let counter = self.function_counter;
474+
self.function_counter += 1;
475+
let mangled = format!("{name}_{}__{counter}", self.code_type().unwrap());
476+
if let Some(scope) = self.function_scope.last_mut() {
477+
scope.insert(name.to_string(), mangled.clone());
478+
}
479+
mangled
480+
} else {
481+
name.to_string()
482+
};
465483
let value = self.module().add_function(&name, r#type, linkage);
466484

467485
if self.debug_info().is_some() {
@@ -524,9 +542,16 @@ impl<'ctx> Context<'ctx> {
524542
name: &str,
525543
is_frontend: bool,
526544
) -> Option<Rc<RefCell<Function<'ctx>>>> {
527-
self.functions
528-
.get(&self.internal_function_name(name, is_frontend))
529-
.cloned()
545+
if is_frontend {
546+
let mangled = self
547+
.function_scope
548+
.iter()
549+
.rev()
550+
.find_map(|scope| scope.get(name))?;
551+
self.functions.get(mangled).cloned()
552+
} else {
553+
self.functions.get(name).cloned()
554+
}
530555
}
531556

532557
/// Returns a shared reference to the current active function.
@@ -654,6 +679,16 @@ impl<'ctx> Context<'ctx> {
654679
.expect("The current context is not in a loop")
655680
}
656681

682+
/// Pushes a new function scope.
683+
pub fn push_function_scope(&mut self) {
684+
self.function_scope.push(BTreeMap::new());
685+
}
686+
687+
/// Pops the current function scope.
688+
pub fn pop_function_scope(&mut self) {
689+
self.function_scope.pop();
690+
}
691+
657692
/// Returns the debug info.
658693
pub fn debug_info(&self) -> Option<&DebugInfo<'ctx>> {
659694
self.debug_info.as_ref()
@@ -1429,15 +1464,6 @@ impl<'ctx> Context<'ctx> {
14291464
)
14301465
}
14311466

1432-
/// Returns the internal function name.
1433-
fn internal_function_name(&self, name: &str, is_frontend: bool) -> String {
1434-
if is_frontend {
1435-
format!("{name}_{}", self.code_type().unwrap())
1436-
} else {
1437-
name.to_string()
1438-
}
1439-
}
1440-
14411467
/// Scans all functions in the module and removes the `MinSize` attribute
14421468
/// if the function contains any large sdiv, udiv, srem, urem instructions with either unknown
14431469
/// NOTE: The check here could be relaxed by checking denominator: if the denominator is

crates/resolc/src/cli_utils.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ pub const YUL_MEMSET_CONTRACT_PATH: &str = "src/tests/data/yul/memset.yul";
2424
pub const YUL_RETURN_CONTRACT_PATH: &str = "src/tests/data/yul/return.yul";
2525
/// The invalid YUL contract test fixture path (hex literal with odd number of nibbles).
2626
pub const YUL_INVALID_HEX_NIBBLES_PATH: &str = "src/tests/data/yul/invalid_hex_nibbles.yul";
27+
/// Yul contract with duplicate function names in switch cases.
28+
pub const YUL_DUPLICATE_FUNCTIONS_SWITCH_PATH: &str =
29+
"src/tests/data/yul/duplicate_functions_switch.yul";
30+
/// Yul contract with duplicate function names in deeply nested switch cases.
31+
pub const YUL_DUPLICATE_FUNCTIONS_DEEP_NESTING_PATH: &str =
32+
"src/tests/data/yul/duplicate_functions_deep_nesting.yul";
2733

2834
/// The standard JSON contracts test fixture path.
2935
pub const STANDARD_JSON_CONTRACTS_PATH: &str =

crates/resolc/src/tests/cli/yul.rs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
33
use crate::cli_utils::{
44
assert_command_failure, assert_command_success, assert_equal_exit_codes, execute_resolc,
5-
execute_solc, RESOLC_YUL_FLAG, SOLC_YUL_FLAG, YUL_CONTRACT_PATH, YUL_INVALID_HEX_NIBBLES_PATH,
5+
execute_solc, RESOLC_YUL_FLAG, SOLC_YUL_FLAG, YUL_CONTRACT_PATH,
6+
YUL_DUPLICATE_FUNCTIONS_DEEP_NESTING_PATH, YUL_DUPLICATE_FUNCTIONS_SWITCH_PATH,
7+
YUL_INVALID_HEX_NIBBLES_PATH,
68
};
79

810
#[test]
@@ -35,3 +37,29 @@ fn bails_with_invalid_input_file() {
3537
let solc_result = execute_solc(&[YUL_INVALID_HEX_NIBBLES_PATH, SOLC_YUL_FLAG]);
3638
assert_equal_exit_codes(&solc_result, &resolc_result);
3739
}
40+
41+
#[test]
42+
fn duplicate_functions_in_switch_cases() {
43+
let resolc_result = execute_resolc(&[
44+
YUL_DUPLICATE_FUNCTIONS_SWITCH_PATH,
45+
RESOLC_YUL_FLAG,
46+
"--bin",
47+
]);
48+
assert_command_success(
49+
&resolc_result,
50+
"Duplicate function names in different switch cases",
51+
);
52+
}
53+
54+
#[test]
55+
fn duplicate_functions_deep_nesting() {
56+
let resolc_result = execute_resolc(&[
57+
YUL_DUPLICATE_FUNCTIONS_DEEP_NESTING_PATH,
58+
RESOLC_YUL_FLAG,
59+
"--bin",
60+
]);
61+
assert_command_success(
62+
&resolc_result,
63+
"Duplicate function names in deeply nested switch cases",
64+
);
65+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
object "Test" {
2+
code {
3+
{
4+
let size := datasize("Test_deployed")
5+
codecopy(0, dataoffset("Test_deployed"), size)
6+
return(0, size)
7+
}
8+
}
9+
object "Test_deployed" {
10+
code {
11+
{
12+
switch calldataload(0)
13+
case 0 {
14+
function f() -> ret {
15+
ret := 1
16+
}
17+
18+
switch calldataload(32)
19+
case 0 {
20+
function g() -> ret {
21+
ret := 10
22+
}
23+
mstore(0, add(f(), g()))
24+
return(0, 32)
25+
}
26+
case 1 {
27+
function g() -> ret {
28+
ret := 20
29+
}
30+
mstore(0, add(f(), g()))
31+
return(0, 32)
32+
}
33+
}
34+
case 1 {
35+
function f() -> ret {
36+
ret := 2
37+
}
38+
39+
switch calldataload(32)
40+
case 0 {
41+
function g() -> ret {
42+
ret := 30
43+
}
44+
mstore(0, add(f(), g()))
45+
return(0, 32)
46+
}
47+
case 1 {
48+
function g() -> ret {
49+
ret := 40
50+
}
51+
mstore(0, add(f(), g()))
52+
return(0, 32)
53+
}
54+
}
55+
}
56+
}
57+
}
58+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
object "Test" {
2+
code {
3+
{
4+
let size := datasize("Test_deployed")
5+
codecopy(0, dataoffset("Test_deployed"), size)
6+
return(0, size)
7+
}
8+
}
9+
object "Test_deployed" {
10+
code {
11+
{
12+
switch calldataload(0)
13+
case 0 {
14+
function f() -> ret {
15+
ret := 1
16+
}
17+
mstore(0, f())
18+
return(0, 32)
19+
}
20+
case 1 {
21+
function f() -> ret {
22+
ret := 2
23+
}
24+
mstore(0, f())
25+
return(0, 32)
26+
}
27+
}
28+
}
29+
}
30+
}

crates/resolc/src/tests/unit/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ mod remappings;
99
mod runtime_code;
1010
mod standard_json;
1111
mod unsupported_opcodes;
12+
mod yul_function_scoping;
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
//! Tests for Yul function scoping and duplicate function name handling.
2+
3+
use crate::test_utils::build_yul;
4+
5+
/// Reproducer from GH-474: duplicate `f` across switch cases.
6+
#[test]
7+
fn duplicate_function_names_in_switch_cases() {
8+
let code = r#"
9+
object "Test" {
10+
code {
11+
{
12+
let size := datasize("Test_deployed")
13+
codecopy(0, dataoffset("Test_deployed"), size)
14+
return(0, size)
15+
}
16+
}
17+
object "Test_deployed" {
18+
code {
19+
{
20+
switch calldataload(0)
21+
case 0 {
22+
function f() -> ret {
23+
ret := 1
24+
}
25+
mstore(0, f())
26+
return(0, 32)
27+
}
28+
case 1 {
29+
function f() -> ret {
30+
ret := 2
31+
}
32+
mstore(0, f())
33+
return(0, 32)
34+
}
35+
}
36+
}
37+
}
38+
}
39+
"#;
40+
41+
build_yul(&[("test.yul", code)])
42+
.expect("should compile duplicate function names in switch cases");
43+
}

crates/yul/src/parser/statement/block.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ impl Block {
136136

137137
impl PolkaVMWriteLLVM for Block {
138138
fn into_llvm(self, context: &mut PolkaVMContext) -> anyhow::Result<()> {
139+
context.push_function_scope();
139140
let current_function = context.current_function().borrow().name().to_owned();
140141
let current_block = context.basic_block();
141142

@@ -223,6 +224,7 @@ impl PolkaVMWriteLLVM for Block {
223224
}
224225

225226
context.pop_debug_scope();
227+
context.pop_function_scope();
226228

227229
Ok(())
228230
}

0 commit comments

Comments
 (0)