mirror of
https://github.com/pezkuwichain/revive.git
synced 2026-10-25 13:15:06 +00:00
This PR introduces powerful code size optimizations. This is achieved with an additional IR layer in between Yul and LLVM IR. It works quite well (see below for a comparison) because it captures optimization gains that neither solc nor LLVM can realize. The feature is gated behind `--newyork` or a corresponding standard JSON input setting and **off by default**. ### How it works We implemented an SSA IR modelled after or inspired by the [MLIR SCF dialect](https://mlir.llvm.org/docs/Dialects/SCFDialect/). Yul is translated into this IR and the IR implements mainly the following optimizations: - **Type narrowing**: narrows 256-bit values to I1..I160 where provable (bidirectional + interprocedural), so LLVM emits native 32/64-bit ops instead of software 256-bit arithmetic. - **FMP range proof**: exposes the `mload(0x40) < heap_size` bound to LLVM IPSCCP, eliminating trailing overflow checks on FMP-derived offsets. Single largest code-size contributor. - **Heap analysis**: skips big-endian byte-swaps where native little-endian layout is safe. - **mem_opt / keccak fusion+folding**, **compound outlining** (mapping sload/sstore), **guard narrowing**, **inlining** tuned for PolkaVM, and **exact + fuzzy function dedup**. ### Results The optimizations implemented with the newyork IR bring substantial code size improvements. - Integration contracts: approximately **-20% to -50%** code size. - OpenZeppelin Wizard corpus: around **-47%** code size. This table shows the code size improvements on various OpenZeppelin ERC implementations, taken from their [contracts wizard](https://wizard.openzeppelin.com/), with almost all features enabled. | Contract | Default: (bytes) | With `--newyork`: (bytes) | Reduction | |---|---:|---:|---:| | erc1155 | 59,206 | 30,649 | -48.2% | | erc20 | 81,697 | 42,893 | -47.5% | | erc721 | 92,052 | 49,423 | -46.3% | | oz_gov | 141,861 | 81,159 | -42.8% | | oz_rwa | 77,274 | 37,975 | -50.9% | | oz_simple_erc20 | 29,950 | 16,554 | -44.7% | | oz_stable | 82,117 | 39,032 | -52.5% | | **Total** | **564,157** | **297,685** | **-47.2%** | We believe this to be a reasonably representative dataset and conclude that the newyork IR achieves drastic code size reductions on real world dApp implementations. ### Methodology, security and runtime performance Since the main contributors to these improvements are the FMP range proofs and the type narrowing, we don't expect big runtime performance regressions (it may even benefit runtime performance in some cases). The PR also includes docs (`book/src/developer_guide/newyork_{optimizer,ir}.md`) and many new integration tests, including regression tests for known FMP soundness traps. The PR was authored collaboratively and reviewed extensively by the `revive` maintainers, as well as Claude Opus, Claude Fable, Qwen, Minimax and Deepseek LLMs, over a span of many months. The `newyork` compiler pipeline introduces no new regressions over our integration and differential test suites (containing over 30000 test cases). This was achieved by careful manual reviews and many LLM bughunt loops. Additionally, a final security review by Anthropic's Fable 5 LLM found no remaining soundness issues. Nonetheless, like any brand new software, please consider this an experimental feature upon release. --------- Signed-off-by: xermicus <bigcyrill@hotmail.com> Signed-off-by: xermicus <cyrill@parity.io> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: neo <me@90f9.org> Co-authored-by: LJ <81748770+elle-j@users.noreply.github.com>
173 lines
9.7 KiB
Rust
173 lines
9.7 KiB
Rust
//! The LLVM context library.
|
|
|
|
#![allow(clippy::too_many_arguments)]
|
|
|
|
use std::ffi::CString;
|
|
use std::sync::OnceLock;
|
|
|
|
pub use self::debug_config::ir_type::IRType as DebugConfigIR;
|
|
pub use self::debug_config::DebugConfig;
|
|
pub use self::optimizer::settings::size_level::SizeLevel as OptimizerSettingsSizeLevel;
|
|
pub use self::optimizer::settings::Settings as OptimizerSettings;
|
|
pub use self::optimizer::Optimizer;
|
|
pub use self::polkavm::build as polkavm_build;
|
|
pub use self::polkavm::context::address_space::AddressSpace as PolkaVMAddressSpace;
|
|
pub use self::polkavm::context::argument::Argument as PolkaVMArgument;
|
|
pub use self::polkavm::context::attribute::Attribute as PolkaVMAttribute;
|
|
pub use self::polkavm::context::attribute::MemoryEffect as PolkaVMMemoryEffect;
|
|
pub use self::polkavm::context::build::Build as PolkaVMBuild;
|
|
pub use self::polkavm::context::code_type::CodeType as PolkaVMCodeType;
|
|
pub use self::polkavm::context::debug_info::DebugInfo;
|
|
pub use self::polkavm::context::function::declaration::Declaration as PolkaVMFunctionDeclaration;
|
|
pub use self::polkavm::context::function::intrinsics::Intrinsics as PolkaVMIntrinsicFunction;
|
|
pub use self::polkavm::context::function::llvm_runtime::LLVMRuntime as PolkaVMLLVMRuntime;
|
|
pub use self::polkavm::context::function::r#return::Return as PolkaVMFunctionReturn;
|
|
pub use self::polkavm::context::function::runtime::arithmetics::Division as PolkaVMDivisionFunction;
|
|
pub use self::polkavm::context::function::runtime::arithmetics::Remainder as PolkaVMRemainderFunction;
|
|
pub use self::polkavm::context::function::runtime::arithmetics::SignedDivision as PolkaVMSignedDivisionFunction;
|
|
pub use self::polkavm::context::function::runtime::arithmetics::SignedRemainder as PolkaVMSignedRemainderFunction;
|
|
pub use self::polkavm::context::function::runtime::deploy_code::DeployCode as PolkaVMDeployCodeFunction;
|
|
pub use self::polkavm::context::function::runtime::entry::Entry as PolkaVMEntryFunction;
|
|
pub use self::polkavm::context::function::runtime::revive::CallDataLoad as PolkaVMCallDataLoadFunction;
|
|
pub use self::polkavm::context::function::runtime::revive::CallValue as PolkaVMCallValueFunction;
|
|
pub use self::polkavm::context::function::runtime::revive::CallValueNonzero as PolkaVMCallValueNonzeroFunction;
|
|
pub use self::polkavm::context::function::runtime::revive::Caller as PolkaVMCallerFunction;
|
|
pub use self::polkavm::context::function::runtime::revive::Exit as PolkaVMExitFunction;
|
|
pub use self::polkavm::context::function::runtime::revive::Revert as PolkaVMRevertFunction;
|
|
pub use self::polkavm::context::function::runtime::revive::RevertEmpty as PolkaVMRevertEmptyFunction;
|
|
pub use self::polkavm::context::function::runtime::revive::RevertPanic as PolkaVMRevertPanicFunction;
|
|
pub use self::polkavm::context::function::runtime::revive::WordToPointer as PolkaVMWordToPointerFunction;
|
|
pub use self::polkavm::context::function::runtime::runtime_code::RuntimeCode as PolkaVMRuntimeCodeFunction;
|
|
pub use self::polkavm::context::function::runtime::sbrk::Sbrk as PolkaVMSbrkFunction;
|
|
pub use self::polkavm::context::function::runtime::FUNCTION_DEPLOY_CODE as PolkaVMFunctionDeployCode;
|
|
pub use self::polkavm::context::function::runtime::FUNCTION_ENTRY as PolkaVMFunctionEntry;
|
|
pub use self::polkavm::context::function::runtime::FUNCTION_RUNTIME_CODE as PolkaVMFunctionRuntimeCode;
|
|
pub use self::polkavm::context::function::yul_data::YulData as PolkaVMFunctionYulData;
|
|
pub use self::polkavm::context::function::Function as PolkaVMFunction;
|
|
pub use self::polkavm::context::global::Global as PolkaVMGlobal;
|
|
pub use self::polkavm::context::pointer::heap::Keccak256OneWord as PolkaVMKeccak256OneWordFunction;
|
|
pub use self::polkavm::context::pointer::heap::Keccak256TwoWords as PolkaVMKeccak256TwoWordsFunction;
|
|
pub use self::polkavm::context::pointer::heap::LoadWord as PolkaVMLoadHeapWordFunction;
|
|
pub use self::polkavm::context::pointer::heap::LoadWordNative as PolkaVMLoadHeapWordNativeFunction;
|
|
pub use self::polkavm::context::pointer::heap::StoreWord as PolkaVMStoreHeapWordFunction;
|
|
pub use self::polkavm::context::pointer::heap::StoreWordNative as PolkaVMStoreHeapWordNativeFunction;
|
|
pub use self::polkavm::context::pointer::storage::LoadTransientWord as PolkaVMLoadTransientStorageWordFunction;
|
|
pub use self::polkavm::context::pointer::storage::LoadWord as PolkaVMLoadStorageWordFunction;
|
|
pub use self::polkavm::context::pointer::storage::StoreTransientWord as PolkaVMStoreTransientStorageWordFunction;
|
|
pub use self::polkavm::context::pointer::storage::StoreWord as PolkaVMStoreStorageWordFunction;
|
|
pub use self::polkavm::context::pointer::Pointer as PolkaVMPointer;
|
|
pub use self::polkavm::context::r#loop::Loop as PolkaVMLoop;
|
|
pub use self::polkavm::context::solidity_data::SolidityData as PolkaVMContextSolidityData;
|
|
pub use self::polkavm::context::yul_data::YulData as PolkaVMContextYulData;
|
|
pub use self::polkavm::context::Context as PolkaVMContext;
|
|
pub use self::polkavm::disassemble as polkavm_disassemble;
|
|
pub use self::polkavm::evm::arithmetic as polkavm_evm_arithmetic;
|
|
pub use self::polkavm::evm::bitwise as polkavm_evm_bitwise;
|
|
pub use self::polkavm::evm::call as polkavm_evm_call;
|
|
pub use self::polkavm::evm::calldata as polkavm_evm_calldata;
|
|
pub use self::polkavm::evm::comparison as polkavm_evm_comparison;
|
|
pub use self::polkavm::evm::context as polkavm_evm_contract_context;
|
|
pub use self::polkavm::evm::create as polkavm_evm_create;
|
|
pub use self::polkavm::evm::crypto as polkavm_evm_crypto;
|
|
pub use self::polkavm::evm::ether_gas as polkavm_evm_ether_gas;
|
|
pub use self::polkavm::evm::event as polkavm_evm_event;
|
|
pub use self::polkavm::evm::event::EventLog as PolkaVMEventLogFunction;
|
|
pub use self::polkavm::evm::ext_code as polkavm_evm_ext_code;
|
|
pub use self::polkavm::evm::immutable as polkavm_evm_immutable;
|
|
pub use self::polkavm::evm::immutable::Load as PolkaVMLoadImmutableDataFunction;
|
|
pub use self::polkavm::evm::immutable::Store as PolkaVMStoreImmutableDataFunction;
|
|
pub use self::polkavm::evm::math as polkavm_evm_math;
|
|
pub use self::polkavm::evm::memory as polkavm_evm_memory;
|
|
pub use self::polkavm::evm::r#return as polkavm_evm_return;
|
|
pub use self::polkavm::evm::return_data as polkavm_evm_return_data;
|
|
pub use self::polkavm::evm::storage as polkavm_evm_storage;
|
|
pub use self::polkavm::hash as polkavm_hash;
|
|
pub use self::polkavm::link as polkavm_link;
|
|
pub use self::polkavm::r#const as polkavm_const;
|
|
pub use self::polkavm::DummyLLVMWritable as PolkaVMDummyLLVMWritable;
|
|
pub use self::polkavm::WriteLLVM as PolkaVMWriteLLVM;
|
|
pub use self::target_machine::target::Target as PolkaVMTarget;
|
|
pub use self::target_machine::TargetMachine as PolkaVMTargetMachine;
|
|
|
|
pub(crate) mod debug_config;
|
|
pub(crate) mod optimizer;
|
|
pub(crate) mod polkavm;
|
|
pub(crate) mod target_machine;
|
|
|
|
static DID_INITIALIZE: OnceLock<()> = OnceLock::new();
|
|
|
|
/// The global LLVM options applied at [`OptimizerSettingsSizeLevel::Z`] **on the newyork path**.
|
|
///
|
|
/// These trade execution speed for code size: the machine outliner replaces repeated
|
|
/// instruction sequences with calls, disabling LICM avoids i256 register pressure on the
|
|
/// PVM target (rv64e) where hoisting causes excessive stack spills, and `ext-tsp-for-size`
|
|
/// optimizes block layout for size instead of speed. At any other optimization level the
|
|
/// upstream LLVM defaults apply: paying execution gas for smaller code is only acceptable
|
|
/// when the user explicitly asked for `-Oz`.
|
|
///
|
|
/// They are applied only for modules produced by the newyork IR generator. Like the
|
|
/// aggressive code-size pass pipeline in [`crate::optimizer::Optimizer::run`] and the
|
|
/// `+unaligned-scalar-mem` target feature, these knobs are tuned and validated against
|
|
/// newyork's outlined code; the stock Yul path keeps the upstream LLVM defaults it was
|
|
/// validated against (matching `main`).
|
|
const SIZE_LEVEL_Z_ARGUMENTS: &[&str] = &[
|
|
"--disable-licm-promotion",
|
|
"--disable-machine-licm",
|
|
"--enable-machine-outliner",
|
|
"--machine-outliner-reruns=2",
|
|
"--disable-early-taildup=true",
|
|
"--apply-ext-tsp-for-size=true",
|
|
"--attributor-allow-deep-wrappers=true",
|
|
"--hoist-common-insts=true",
|
|
];
|
|
|
|
/// Initializes the LLVM compiler backend.
|
|
///
|
|
/// This is a no-op if called subsequentially.
|
|
///
|
|
/// `llvm_arguments` are passed as-is to the LLVM CL options parser.
|
|
///
|
|
/// When `use_newyork` is set and the size level is [`OptimizerSettingsSizeLevel::Z`], the
|
|
/// `SIZE_LEVEL_Z_ARGUMENTS` are prepended. They are gated on the newyork path because they
|
|
/// are validated against its codegen; the stock Yul path must keep the upstream defaults.
|
|
/// LLVM parses command line options once per process, so the first caller wins; production
|
|
/// builds are unaffected because every contract is compiled in its own recursive process
|
|
/// with its own optimizer settings and IR generator.
|
|
pub fn initialize_llvm(
|
|
target: PolkaVMTarget,
|
|
name: &str,
|
|
size_level: OptimizerSettingsSizeLevel,
|
|
use_newyork: bool,
|
|
llvm_arguments: &[String],
|
|
) {
|
|
let Ok(_) = DID_INITIALIZE.set(()) else {
|
|
return; // Tests don't go through a recursive process
|
|
};
|
|
|
|
let size_arguments = if use_newyork && size_level == OptimizerSettingsSizeLevel::Z {
|
|
SIZE_LEVEL_Z_ARGUMENTS
|
|
} else {
|
|
&[]
|
|
};
|
|
let argv = std::iter::once(name)
|
|
.chain(size_arguments.iter().copied())
|
|
.chain(llvm_arguments.iter().map(String::as_str))
|
|
.map(|arg| CString::new(arg.as_bytes()).unwrap())
|
|
.collect::<Vec<_>>();
|
|
let argv: Vec<*const libc::c_char> = argv.iter().map(|arg| arg.as_ptr()).collect();
|
|
let overview = CString::new("").unwrap();
|
|
unsafe {
|
|
inkwell::llvm_sys::support::LLVMParseCommandLineOptions(
|
|
argv.len() as i32,
|
|
argv.as_ptr(),
|
|
overview.as_ptr(),
|
|
);
|
|
}
|
|
|
|
inkwell::support::enable_llvm_pretty_stack_trace();
|
|
|
|
match target {
|
|
PolkaVMTarget::PVM => inkwell::targets::Target::initialize_riscv(&Default::default()),
|
|
}
|
|
}
|