mirror of
https://github.com/pezkuwichain/revive.git
synced 2026-10-20 18:56:17 +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>
116 lines
4.3 KiB
Rust
116 lines
4.3 KiB
Rust
use std::{env, fs, path::Path, process::Command};
|
|
|
|
const TARGET_TRIPLE_FLAG: &str = "-triple=riscv64-unknown-unknown-elf";
|
|
const TARGET_FLAG: &str = "--target=riscv64";
|
|
const TARGET_ARCH_FLAG: &str = "-march=rv64emac";
|
|
const TARGET_ABI_FLAG: &str = "-mabi=lp64e";
|
|
|
|
/// RISC-V backend features enabled for the runtime-API helpers (`memcpy`,
|
|
/// `memmove`, `memset` and the syscall stubs).
|
|
///
|
|
/// Each feature MUST be passed as its own `-target-feature` flag. A single
|
|
/// comma-joined value (`+a,+b,...`) does not work: clang treats one
|
|
/// `-target-feature` argument as one feature and appends its own
|
|
/// `-march`-derived feature list *after* it, so under LLVM's last-one-wins
|
|
/// merge the comma blob is silently cancelled — e.g. `+xtheadcondmov` and
|
|
/// `+zbb` end up disabled.
|
|
///
|
|
/// The list mirrors `TargetMachine::VM_FEATURES` in `revive-llvm-context`
|
|
/// (kept in sync by hand — a build script can't depend on that crate). It
|
|
/// deliberately omits `+unaligned-scalar-mem`: these helpers are linked into
|
|
/// every contract and must stay inline-compatible with the **stock** Yul path,
|
|
/// whose target machine does not enable that feature (newyork appends it only
|
|
/// for its own code). LLVM refuses to inline a callee whose feature set is not
|
|
/// a subset of the caller's, so a helper carrying `+unaligned-scalar-mem`
|
|
/// would fail to inline into stock-path code. The feature does not change the
|
|
/// helpers' own codegen, so omitting it costs nothing.
|
|
const TARGET_FEATURES: &[&str] = &[
|
|
"+e",
|
|
"+m",
|
|
"+a",
|
|
"+c",
|
|
"+zbb",
|
|
"+auipc-addi-fusion",
|
|
"+ld-add-fusion",
|
|
"+lui-addi-fusion",
|
|
"+xtheadcondmov",
|
|
"+relax",
|
|
];
|
|
|
|
const IMPORTS_SOUCE: &str = "src/polkavm_imports.c";
|
|
const IMPORTS_BC: &str = "polkavm_imports.bc";
|
|
const IMPORTS_RUST: &str = "polkavm_imports.rs";
|
|
|
|
const EXPORTS_SOUCE: &str = "src/polkavm_exports.c";
|
|
const EXPORTS_BC: &str = "polkavm_exports.bc";
|
|
const EXPORTS_RUST: &str = "polkavm_exports.rs";
|
|
|
|
fn compile(source_path: &str, bitcode_path: &str) {
|
|
let mut args: Vec<&str> = vec![
|
|
TARGET_FLAG,
|
|
"-Xclang",
|
|
TARGET_TRIPLE_FLAG,
|
|
TARGET_ARCH_FLAG,
|
|
TARGET_ABI_FLAG,
|
|
];
|
|
for feature in TARGET_FEATURES {
|
|
args.extend(["-Xclang", "-target-feature", "-Xclang", feature]);
|
|
}
|
|
args.extend([
|
|
"-fno-exceptions",
|
|
"-ffreestanding",
|
|
"-Wall",
|
|
"-fno-builtin",
|
|
"-O3",
|
|
"-emit-llvm",
|
|
"-c",
|
|
"-o",
|
|
bitcode_path,
|
|
source_path,
|
|
]);
|
|
|
|
let output = Command::new(revive_build_utils::llvm_host_tool("clang"))
|
|
.args(&args)
|
|
.output()
|
|
.unwrap_or_else(|error| panic!("failed to execute clang: {error}"));
|
|
|
|
assert!(
|
|
output.status.success(),
|
|
"failed to compile the PolkaVM C API: {output:?}"
|
|
);
|
|
}
|
|
|
|
fn build_module(source_path: &str, bitcode_path: &str, rust_file: &str) {
|
|
let out_dir = env::var_os("OUT_DIR").expect("env should have $OUT_DIR");
|
|
let lib = Path::new(&out_dir).join(bitcode_path);
|
|
compile(source_path, lib.to_str().expect("$OUT_DIR should be UTF-8"));
|
|
|
|
// `inkwell::MemoryBuffer::create_from_memory_range` requires a trailing nul byte
|
|
// (it subtracts one from the length to drop the terminator), so we embed the bitcode
|
|
// with an extra nul byte appended.
|
|
let mut bitcode = fs::read(lib).expect("bitcode should have been built");
|
|
bitcode.push(0);
|
|
let padded_name = format!("{bitcode_path}.nul");
|
|
let padded_path = Path::new(&out_dir).join(&padded_name);
|
|
fs::write(&padded_path, &bitcode).expect("should be able to write in $OUT_DIR");
|
|
let len = bitcode.len();
|
|
let src_path = Path::new(&out_dir).join(rust_file);
|
|
let src = format!("pub static BITCODE: &[u8; {len}] = include_bytes!(\"{padded_name}\");");
|
|
fs::write(src_path, src).expect("should be able to write in $OUT_DIR");
|
|
}
|
|
|
|
fn main() {
|
|
println!(
|
|
"cargo:rerun-if-env-changed={}",
|
|
revive_build_utils::REVIVE_LLVM_HOST_PREFIX
|
|
);
|
|
|
|
build_module(IMPORTS_SOUCE, IMPORTS_BC, IMPORTS_RUST);
|
|
build_module(EXPORTS_SOUCE, EXPORTS_BC, EXPORTS_RUST);
|
|
|
|
println!("cargo:rerun-if-changed=build.rs");
|
|
println!("cargo:rerun-if-changed=src/polkavm_imports.c");
|
|
println!("cargo:rerun-if-changed=src/polkavm_exports.c");
|
|
println!("cargo:rerun-if-changed=src/polkavm_guest.h");
|
|
}
|