Skip to content

ICE same-named Yul functions in sibling blocks #474

@elle-j

Description

@elle-j

Description

The compiler crashes with an Internal Compiler Error (ICE) when compiling valid Yul code that defines functions with the same name in separate sibling blocks using --yul mode. This includes e.g. switch case blocks and if blocks. This is valid Yul because functions are scoped to their enclosing block, but the compiler treats them as duplicate declarations.

Note: This bug does not affect --standard-json mode because this code path goes through solc to get the optimized IR, and solc renames the duplicate function names (see section "Why Standard JSON Mode Is Not Affected").

Detected when trying to compile: resolc-compiler-tests/fixtures/yul/semantic/function_definitions.yul

Note

Related (but different) issue: #475

Minimal Reproduction

1. Create a Yul file that triggers the ICE

cat > ice-name-conflict-in-switch.yul << 'EOF'
object "Test" {
    code {
        {
            let size := datasize("Test_deployed")
            codecopy(0, dataoffset("Test_deployed"), size)
            return(0, size)
        }
    }
    object "Test_deployed" {
        code {
            {
                switch calldataload(0)
                case 0 {
                    function f() -> ret {
                        ret := 1
                    }
                    mstore(0, f())
                    return(0, 32)
                }
                case 1 {
                    function f() -> ret {
                        ret := 2
                    }
                    mstore(0, f())
                    return(0, 32)
                }
            }
        }
    }
}
EOF

2. Compile the file with resolc

resolc --yul ice-name-conflict-in-switch.yul --bin

3. For comparison, compile the file with solc

solc --strict-assembly ice-name-conflict-in-switch.yul --bin

Expected Outcome

The compiler should output PVM bytecode. The two f functions are in separate case blocks and should not conflict.

Actual Outcome

solc outputs EVM bytecode.

resolc panics with an ICE:

Error: "/path/to/bin/resolc" subprocess failed with exit code Some(101):


thread '<unnamed>' (37651576) panicked at crates/llvm-context/src/polkavm/context/mod.rs:459:9:
ICE: function 'f' declared subsequentally
stack backtrace:
   0: __rustc::rust_begin_unwind
   1: core::panicking::panic_fmt
   2: revive_llvm_context::polkavm::context::Context::add_function
   3: <revive_yul::parser::statement::function_definition::FunctionDefinition as revive_llvm_context::polkavm::WriteLLVM>::declare
   4: <revive_yul::parser::statement::block::Block as revive_llvm_context::polkavm::WriteLLVM>::into_llvm
   5: <revive_yul::parser::statement::switch::Switch as revive_llvm_context::polkavm::WriteLLVM>::into_llvm
   6: <revive_yul::parser::statement::block::Block as revive_llvm_context::polkavm::WriteLLVM>::into_llvm
   7: <revive_yul::parser::statement::block::Block as revive_llvm_context::polkavm::WriteLLVM>::into_llvm
   8: <revive_llvm_context::polkavm::context::function::runtime::runtime_code::RuntimeCode<B> as revive_llvm_context::polkavm::WriteLLVM>::into_llvm
   9: <revive_yul::parser::statement::object::Object as revive_llvm_context::polkavm::WriteLLVM>::into_llvm
  10: <revive_yul::parser::statement::object::Object as revive_llvm_context::polkavm::WriteLLVM>::into_llvm
  11: resolc::project::contract::Contract::compile
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

thread 'main' (37651563) panicked at crates/resolc/src/process/native_process.rs:51:14:
Threading error: Any { .. }
stack backtrace:
   0: __rustc::rust_begin_unwind
   1: core::panicking::panic_fmt
   2: core::result::unwrap_failed
   3: <resolc::process::native_process::NativeProcess as resolc::process::Process>::run
   4: resolc::main_inner
   5: resolc::main
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
--> ice-name-conflict-in-switch.yul:Test

Why Standard JSON Mode Is Not Affected

When using --standard-json mode we first go through solc getting the optimized IR. Depending on whether optimizer.enabled is true or false, solc resolves the duplicate function names differently:

With optimizer enabled (simple functions are inlined):

// Original
case 0 {
    function f() -> ret { ret := 1 }
    mstore(0, f())
}
case 1 {
    function f() -> ret { ret := 2 }
    mstore(0, f())
}

// `irOptimized`
case 0 {
    mstore(0, 1)
    return(0, 32)
}
case 1 {
    mstore(0, 2)
    return(0, 32)
}

With optimizer disabled (functions are hoisted to the code block level and duplicate names are renamed):

// `irOptimized`
{
    switch calldataload(0)
    case 0 {
        mstore(0, f())
        return(0, 32)
    }
    case 1 {
        mstore(0, f_1())  // Renamed!
        return(0, 32)
    }
}
function f() -> ret { ret := 1 }
function f_1() -> ret { ret := 2 }  // Renamed!

1. Create the standard JSON input file

Using the ice-name-conflict-in-switch.yul file created in the earlier section:

cat > ice-name-conflict-in-switch.json << EOF
{
  "language": "Yul",
  "sources": {
    "ice-name-conflict-in-switch.yul": {
      "content": $(cat ice-name-conflict-in-switch.yul | jq -Rs .)
    }
  },
  "settings": {
    "optimizer": {
      "enabled": true
    },
    "outputSelection": {
      "*": {
        "*": ["evm.bytecode"]
      }
    }
  }
}
EOF

2. Compile the file with resolc

resolc --standard-json < ice-name-conflict-in-switch.json

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions