llvm-context: modularize compiler builtin functions (#234)

- Add the revive runtime function interface to minimize boiler plate
code.
- Outline heavily repeated code into dedicated functions to bring down
code size.
- The code size tests builds optimized for size.
- Function attributes are passed as slices.

This significantly brings down the code size for all OpenZeppelin wizard
contracts (using all possible features) compiled against OpenZeppelin
`v5.0.0` with size optimizations.

|contract|| `-Oz` main | `-Oz` PR || `-O3` main | `-O3` PR |
|-|-|-|-|-|-|-|
|erc1155.sol||100K|67K||114K|147K|
|erc20.sol||120K|90K||160K|191K|
|erc721.sol||128K|101K||178K|214K|
|governor.sol||226K|165K||293K|349K|
|rwa.sol||116K|85K||154K|185K|
|stable.sol||116K|86K||155K|192K|

On the flip side this introduces a heavy penalty for cycle optimized
builds. Setting the no-inline attributes for cycle optimized builds
helps a lot but heavily penalizes runtime speed (LLVM does not yet
inline everything properly - to be investigated later on).

Next steps:
- Modularize more functions
- Refactor the YUL function arguments to use pointers instead of values
- Afterwards check if LLVM still has trouble inline-ing properly on O3
or set the no-inline attribute if it does not penalize runtime
performance too bad.
This commit is contained in:
xermicus
2025-02-25 16:47:01 +01:00
committed by GitHub
parent 7ffe64ed7c
commit a07968205b
32 changed files with 1444 additions and 655 deletions
+25 -107
View File
@@ -2,8 +2,13 @@
use inkwell::values::BasicValue;
use crate::polkavm::context::runtime::RuntimeFunction;
use crate::polkavm::context::Context;
use crate::polkavm::Dependency;
use crate::PolkaVMDivisionFunction;
use crate::PolkaVMRemainderFunction;
use crate::PolkaVMSignedDivisionFunction;
use crate::PolkaVMSignedRemainderFunction;
/// Translates the arithmetic addition.
pub fn addition<'ctx, D>(
@@ -59,11 +64,11 @@ pub fn division<'ctx, D>(
where
D: Dependency + Clone,
{
wrapped_division(context, operand_2, || {
Ok(context
.builder()
.build_int_unsigned_div(operand_1, operand_2, "DIV")?)
})
let name = <PolkaVMDivisionFunction as RuntimeFunction<D>>::NAME;
let declaration = <PolkaVMDivisionFunction as RuntimeFunction<D>>::declaration(context);
Ok(context
.build_call(declaration, &[operand_1.into(), operand_2.into()], "div")
.unwrap_or_else(|| panic!("revive runtime function {name} should return a value",)))
}
/// Translates the arithmetic remainder.
@@ -75,11 +80,11 @@ pub fn remainder<'ctx, D>(
where
D: Dependency + Clone,
{
wrapped_division(context, operand_2, || {
Ok(context
.builder()
.build_int_unsigned_rem(operand_1, operand_2, "MOD")?)
})
let name = <PolkaVMRemainderFunction as RuntimeFunction<D>>::NAME;
let declaration = <PolkaVMRemainderFunction as RuntimeFunction<D>>::declaration(context);
Ok(context
.build_call(declaration, &[operand_1.into(), operand_2.into()], "rem")
.unwrap_or_else(|| panic!("revive runtime function {name} should return a value",)))
}
/// Translates the signed arithmetic division.
@@ -94,54 +99,11 @@ pub fn division_signed<'ctx, D>(
where
D: Dependency + Clone,
{
assert_eq!(
operand_2.get_type().get_bit_width(),
revive_common::BIT_LENGTH_WORD as u32
);
let block_calculate = context.append_basic_block("calculate");
let block_overflow = context.append_basic_block("overflow");
let block_select = context.append_basic_block("select_result");
let block_origin = context.basic_block();
context.builder().build_switch(
operand_2,
block_calculate,
&[
(context.word_type().const_zero(), block_select),
(context.word_type().const_all_ones(), block_overflow),
],
)?;
context.set_basic_block(block_calculate);
let quotient = context
.builder()
.build_int_signed_div(operand_1, operand_2, "SDIV")?;
context.build_unconditional_branch(block_select);
context.set_basic_block(block_overflow);
let max_uint = context.builder().build_int_z_extend(
context
.integer_type(revive_common::BIT_LENGTH_WORD - 1)
.const_all_ones(),
context.word_type(),
"max_uint",
)?;
let is_operand_1_overflow = context.builder().build_int_compare(
inkwell::IntPredicate::EQ,
operand_1,
context.builder().build_int_neg(max_uint, "min_uint")?,
"is_operand_1_overflow",
)?;
context.build_conditional_branch(is_operand_1_overflow, block_select, block_calculate)?;
context.set_basic_block(block_select);
let result = context.builder().build_phi(context.word_type(), "result")?;
result.add_incoming(&[
(&operand_1, block_overflow),
(&context.word_const(0), block_origin),
(&quotient.as_basic_value_enum(), block_calculate),
]);
Ok(result.as_basic_value())
let name = <PolkaVMSignedDivisionFunction as RuntimeFunction<D>>::NAME;
let declaration = <PolkaVMSignedDivisionFunction as RuntimeFunction<D>>::declaration(context);
Ok(context
.build_call(declaration, &[operand_1.into(), operand_2.into()], "sdiv")
.unwrap_or_else(|| panic!("revive runtime function {name} should return a value",)))
}
/// Translates the signed arithmetic remainder.
@@ -153,53 +115,9 @@ pub fn remainder_signed<'ctx, D>(
where
D: Dependency + Clone,
{
wrapped_division(context, operand_2, || {
Ok(context
.builder()
.build_int_signed_rem(operand_1, operand_2, "SMOD")?)
})
}
/// Wrap division operations so that zero will be returned if the
/// denominator is zero (see also Ethereum YP Appendix H.2).
///
/// The closure is expected to calculate and return the quotient.
///
/// The result is either the calculated quotient or zero,
/// selected at runtime.
fn wrapped_division<'ctx, D, F, T>(
context: &Context<'ctx, D>,
denominator: inkwell::values::IntValue<'ctx>,
f: F,
) -> anyhow::Result<inkwell::values::BasicValueEnum<'ctx>>
where
D: Dependency + Clone,
F: FnOnce() -> anyhow::Result<T>,
T: inkwell::values::IntMathValue<'ctx>,
{
assert_eq!(
denominator.get_type().get_bit_width(),
revive_common::BIT_LENGTH_WORD as u32
);
let block_calculate = context.append_basic_block("calculate");
let block_select = context.append_basic_block("select");
let block_origin = context.basic_block();
context.builder().build_switch(
denominator,
block_calculate,
&[(context.word_const(0), block_select)],
)?;
context.set_basic_block(block_calculate);
let calculated_value = f()?.as_basic_value_enum();
context.build_unconditional_branch(block_select);
context.set_basic_block(block_select);
let result = context.builder().build_phi(context.word_type(), "result")?;
result.add_incoming(&[
(&context.word_const(0), block_origin),
(&calculated_value, block_calculate),
]);
Ok(result.as_basic_value())
let name = <PolkaVMSignedRemainderFunction as RuntimeFunction<D>>::NAME;
let declaration = <PolkaVMSignedRemainderFunction as RuntimeFunction<D>>::declaration(context);
Ok(context
.build_call(declaration, &[operand_1.into(), operand_2.into()], "srem")
.unwrap_or_else(|| panic!("revive runtime function {name} should return a value",)))
}