mirror of
https://github.com/pezkuwichain/revive.git
synced 2026-10-20 18:56:17 +00:00
The new Yul optimizer toolkit (#519)
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>
This commit is contained in:
183 files changed
+49503
-561
No files matched your search
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "revive-newyork"
|
||||
description = "NEW Yul OptimizIR Kit - Custom IR layer for domain-specific optimizations"
|
||||
version = "1.0.0"
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
repository.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
log = { workspace = true }
|
||||
num = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
inkwell = { workspace = true }
|
||||
|
||||
revive-yul = { workspace = true }
|
||||
revive-llvm-context = { workspace = true }
|
||||
revive-common = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,283 @@
|
||||
//! NEW Yul OptimizIR Kit (newyork)
|
||||
//!
|
||||
//! This crate provides a custom intermediate representation (IR) for the Revive
|
||||
//! compiler, positioned between Yul and LLVM IR. It enables domain-specific
|
||||
//! optimizations that LLVM cannot perform because it lacks semantic knowledge
|
||||
//! of the PolkaVM target and EVM/Solidity source domains.
|
||||
//!
|
||||
//! # Architecture
|
||||
//!
|
||||
//! ```text
|
||||
//! Yul AST ──► newyork IR ──► [Optimizations] ──► LLVM IR ──► RISC-V
|
||||
//! (from_yul) (passes) (to_llvm)
|
||||
//! ```
|
||||
//!
|
||||
//! # Design Principles
|
||||
//!
|
||||
//! 1. **SSA with Structured Control Flow** - Preserves high-level structure from Yul
|
||||
//! 2. **Explicit Types with Address Spaces** - Every value has a known bit-width
|
||||
//! 3. **Pure Expressions vs Effectful Statements** - Enables easier reasoning
|
||||
//! 4. **Semantic Annotations** - Storage/memory operations tagged with region info
|
||||
|
||||
pub mod from_yul;
|
||||
pub mod guard_narrow;
|
||||
pub mod heap_opt;
|
||||
pub mod inline;
|
||||
pub mod ir;
|
||||
pub mod mapping_access_outlining;
|
||||
pub mod mem_opt;
|
||||
pub mod printer;
|
||||
pub mod simplify;
|
||||
pub mod ssa;
|
||||
pub mod to_llvm;
|
||||
pub mod type_inference;
|
||||
pub mod validate;
|
||||
|
||||
pub use from_yul::{TranslationError, YulTranslator};
|
||||
pub use heap_opt::{
|
||||
AccessPattern, HeapAnalysis, HeapAnalysisStats, HeapOptResults, MemorySlot, OffsetInfo,
|
||||
};
|
||||
pub use inline::{
|
||||
analyze_call_graph, inline_functions, CallGraphAnalysis, InlineDecision, InlineResults,
|
||||
};
|
||||
pub use ir::{
|
||||
AddressSpace, BinaryOperation, BitWidth, Block, CallKind, CreateKind, Expression, Function,
|
||||
FunctionId, MemoryRegion, Object, Region, Statement, SwitchCase, Type, UnaryOperation, Value,
|
||||
ValueId,
|
||||
};
|
||||
pub use mem_opt::{MemOptResults, MemoryOptimizer};
|
||||
pub use printer::{
|
||||
print_expression, print_function, print_object, print_object_with_types, print_statement,
|
||||
Printer, PrinterConfig,
|
||||
};
|
||||
pub use simplify::{
|
||||
deduplicate_functions, deduplicate_functions_fuzzy, fold_constant_keccak, Simplifier,
|
||||
SimplifyResults,
|
||||
};
|
||||
pub use ssa::SsaBuilder;
|
||||
pub use to_llvm::{CodegenError, LlvmCodegen};
|
||||
pub use type_inference::{TypeConstraint, TypeInference};
|
||||
pub use validate::{validate_object, ValidationError, ValidationResult};
|
||||
|
||||
/// Result of translating a Yul object to newyork IR.
|
||||
pub struct TranslationResult {
|
||||
/// The translated IR object.
|
||||
pub object: Object,
|
||||
/// Heap optimization results (identifies where byte-swapping can be skipped).
|
||||
pub heap_opt: HeapOptResults,
|
||||
/// Type inference results (narrower types for values).
|
||||
pub type_info: TypeInference,
|
||||
/// Memory optimization results (load-after-store, dead store elimination).
|
||||
pub mem_opt: MemOptResults,
|
||||
/// Inlining results (which functions were inlined and removed).
|
||||
pub inline_results: InlineResults,
|
||||
/// The IR printed after the intra-object optimization passes, before the late passes.
|
||||
/// `Some` only when `capture_ir_snapshot` was requested.
|
||||
pub ir_snapshot: Option<String>,
|
||||
}
|
||||
|
||||
/// Translates a Yul object to newyork IR.
|
||||
///
|
||||
/// This is the main entry point for converting Yul AST to the new IR format.
|
||||
/// Returns the IR object along with heap optimization analysis results.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```ignore
|
||||
/// use revive_newyork::translate_yul_object;
|
||||
/// use revive_yul::parser::statement::object::Object;
|
||||
///
|
||||
/// let yul_object: Object = /* parse yul */;
|
||||
/// let result = translate_yul_object(&yul_object, false)?;
|
||||
/// let ir_object = result.object;
|
||||
/// let heap_opt = result.heap_opt;
|
||||
/// ```
|
||||
pub fn translate_yul_object(
|
||||
yul_object: &revive_yul::parser::statement::object::Object,
|
||||
capture_ir_snapshot: bool,
|
||||
) -> Result<TranslationResult, TranslationError> {
|
||||
let mut translator = YulTranslator::new();
|
||||
let mut ir_object = translator.translate_object(yul_object)?;
|
||||
|
||||
let (mut inline_results, mem_opt_results) = optimize_object_tree(&mut ir_object);
|
||||
|
||||
let ir_snapshot = capture_ir_snapshot.then(|| print_object(&ir_object));
|
||||
|
||||
let mut type_info = TypeInference::new();
|
||||
type_info.infer_object_tree(&ir_object);
|
||||
|
||||
let type_info = type_inference::narrow_signatures_to_fixed_point(&mut ir_object, type_info);
|
||||
|
||||
let type_info = run_late_inline_loop(&mut ir_object, &mut inline_results, type_info);
|
||||
|
||||
let heap_opt = ir_object.analyze_heap();
|
||||
let (type_info, heap_opt) = reinfer_for_unbounded_fmp(&mut ir_object, type_info, heap_opt);
|
||||
|
||||
if let Err(errors) = validate::validate_object(&ir_object) {
|
||||
let details = errors
|
||||
.iter()
|
||||
.map(|error| format!(" - {error}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
panic!(
|
||||
"ICE: IR validation failed for object `{}` after optimization pipeline:\n{}",
|
||||
ir_object.name, details,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(TranslationResult {
|
||||
object: ir_object,
|
||||
heap_opt,
|
||||
type_info,
|
||||
mem_opt: mem_opt_results,
|
||||
inline_results,
|
||||
ir_snapshot,
|
||||
})
|
||||
}
|
||||
|
||||
/// Maximum number of late inline + simplify + narrow iterations.
|
||||
///
|
||||
/// One round captures every additional inline the cost model can make once the pipeline's other
|
||||
/// passes have settled (measured on the OZ corpus — two or more rounds produce identical output).
|
||||
/// The bound is kept as a constant so future cost-model work that depends on cascading shrinkage
|
||||
/// across iterations doesn't have to re-introduce the scaffolding.
|
||||
const LATE_INLINE_ITERATIONS: u32 = 1;
|
||||
|
||||
/// Runs a fixed-point loop of late inline + simplify + dedup + type narrow.
|
||||
///
|
||||
/// Now that parameter narrowing has propagated through the IR and simplification has folded any
|
||||
/// newly exposed constants, some wrapper functions have shrunk below the inline thresholds. After
|
||||
/// the early inline + heap + mem_opt + mapping_access_outlining + guard_narrow + first round of param
|
||||
/// narrowing has completed, many wrapper helpers have collapsed to a handful of statements. The
|
||||
/// early inliner couldn't act on them because they were still wrapped in pre-simplify noise;
|
||||
/// running the inliner again at this point — with re-estimated function sizes and on top of the
|
||||
/// narrowed signatures — picks up the residue.
|
||||
///
|
||||
/// This is intentionally separate from the early inliner: the early pass exposes intra-procedural
|
||||
/// opportunities that drive the rest of the pipeline; the late pass collects the per-function
|
||||
/// shrinkage produced by every subsequent optimization (mem_opt, mapping_access_outlining, guard_narrow,
|
||||
/// full type narrowing).
|
||||
///
|
||||
/// Within the loop, compound outlining + guard narrowing are re-run after inlining because the
|
||||
/// inlined bodies expose new keccak256_pair+sload pairs and overflow checks that the early pass
|
||||
/// couldn't see across the call boundary. A final size refresh follows the loop so LLVM-level
|
||||
/// inline hints (set during codegen) see the post-simplify, post-narrow shape rather than the
|
||||
/// larger pre-cleanup estimates that `inline_functions` last set internally.
|
||||
///
|
||||
/// The function takes ownership of `type_info` so we can reseed it after the inlined IR is in
|
||||
/// place; the returned `TypeInference` reflects the final state used for downstream codegen.
|
||||
fn run_late_inline_loop(
|
||||
ir_object: &mut ir::Object,
|
||||
inline_results: &mut InlineResults,
|
||||
mut type_info: TypeInference,
|
||||
) -> TypeInference {
|
||||
for _ in 0..LATE_INLINE_ITERATIONS {
|
||||
inline::estimate_function_sizes(ir_object);
|
||||
*inline_results += inline_functions(ir_object);
|
||||
|
||||
let mut simplifier = Simplifier::new();
|
||||
simplifier.simplify_object(ir_object);
|
||||
|
||||
mapping_access_outlining::outline_mapping_accesses_in_object(ir_object);
|
||||
guard_narrow::narrow_guards_in_object(ir_object);
|
||||
let mut simplifier_post = Simplifier::new();
|
||||
simplifier_post.simplify_object(ir_object);
|
||||
|
||||
let _ = deduplicate_functions(ir_object);
|
||||
let _ = simplify::deduplicate_functions_fuzzy(ir_object);
|
||||
|
||||
type_info = TypeInference::new();
|
||||
type_info.infer_object_tree(ir_object);
|
||||
type_info = type_inference::narrow_signatures_to_fixed_point(ir_object, type_info);
|
||||
}
|
||||
inline::estimate_function_sizes(ir_object);
|
||||
type_info
|
||||
}
|
||||
|
||||
/// Re-infers types with `FreePointerSlot` `mload` widths forced to I256 when `heap_opt` reports the
|
||||
/// FMP word may hold a non-bounded value.
|
||||
///
|
||||
/// `heap_opt` runs after the type-narrowing fixed point, so the first inference assumed the sound-
|
||||
/// only-when-bounded I32 FMP width. When the FMP could be unbounded, codegen loads the full FMP word
|
||||
/// (no `FMP < heap_size` range proof); the I32 forward width would then let a downstream comparison
|
||||
/// or offset bare-truncate the live value. Re-inferring with the flag set keeps the load — and every
|
||||
/// value derived from it — full width. The IR is already at its narrowing fixed point, so re-running
|
||||
/// `narrow_signatures` only reproduces the same parameter types (it never un-narrows); `heap_opt` is
|
||||
/// recomputed in case it shifted. Bounded FMPs (the Solidity allocator, all of OZ) skip this and keep
|
||||
/// the I32 width.
|
||||
fn reinfer_for_unbounded_fmp(
|
||||
ir_object: &mut ir::Object,
|
||||
type_info: TypeInference,
|
||||
heap_opt: HeapOptResults,
|
||||
) -> (TypeInference, HeapOptResults) {
|
||||
if !heap_opt.fmp_could_be_unbounded() {
|
||||
return (type_info, heap_opt);
|
||||
}
|
||||
let mut reinferred = TypeInference::new();
|
||||
reinferred.set_fmp_could_be_unbounded(true);
|
||||
reinferred.infer_object_tree(ir_object);
|
||||
let reinferred = type_inference::narrow_signatures_to_fixed_point(ir_object, reinferred);
|
||||
let heap_opt = ir_object.analyze_heap();
|
||||
(reinferred, heap_opt)
|
||||
}
|
||||
|
||||
/// Runs the full newyork optimization pipeline on an object and its subobjects.
|
||||
///
|
||||
/// Pass order matters: inlining runs first to expose intra-procedural opportunities,
|
||||
/// then simplify+dedup clean up the IR, then mem_opt + FMP propagation expose
|
||||
/// constant keccak inputs, and finally compound outlining and guard narrowing
|
||||
/// rewrite specialized patterns. Each of those rewriting passes can produce new
|
||||
/// constants and dead code, so a simplify pass follows each cluster; a second dedup
|
||||
/// catches near-duplicates that only emerge after canonicalization.
|
||||
///
|
||||
/// Subobjects are processed recursively and their inline/memory results are merged
|
||||
/// into the returned aggregates.
|
||||
fn optimize_object_tree(object: &mut ir::Object) -> (InlineResults, MemOptResults) {
|
||||
let mut inline_results = inline_functions(object);
|
||||
|
||||
let mut simplifier = Simplifier::new();
|
||||
let _simplify_stats = simplifier.simplify_object(object);
|
||||
|
||||
let _dedup_count = deduplicate_functions(object);
|
||||
let _fuzzy_dedup_count = simplify::deduplicate_functions_fuzzy(object);
|
||||
|
||||
let mut mem_optimizer = MemoryOptimizer::new();
|
||||
let mut mem_opt_results = mem_optimizer.optimize_object(object);
|
||||
let mut fmp_prop = mem_opt::FmpPropagation::new();
|
||||
fmp_prop.propagate_object(object);
|
||||
mem_opt_results.fmp_loads_eliminated += fmp_prop.loads_eliminated;
|
||||
|
||||
simplify::fold_constant_keccak(object);
|
||||
|
||||
let mut simplifier2 = Simplifier::new();
|
||||
simplifier2.simplify_object(object);
|
||||
|
||||
mapping_access_outlining::outline_mapping_accesses_in_object(object);
|
||||
guard_narrow::narrow_guards_in_object(object);
|
||||
|
||||
let mut simplifier3 = Simplifier::new();
|
||||
simplifier3.simplify_object(object);
|
||||
|
||||
let _eliminated = inline::eliminate_constant_parameters(object);
|
||||
if _eliminated > 0 {
|
||||
let mut simplifier_post_const = Simplifier::new();
|
||||
simplifier_post_const.simplify_object(object);
|
||||
}
|
||||
|
||||
let _predicted = inline::inline_by_shrink_prediction(object);
|
||||
if _predicted > 0 {
|
||||
let mut simplifier_post_predict = Simplifier::new();
|
||||
simplifier_post_predict.simplify_object(object);
|
||||
}
|
||||
|
||||
let _dedup_count2 = deduplicate_functions(object);
|
||||
let _fuzzy_dedup_count2 = simplify::deduplicate_functions_fuzzy(object);
|
||||
|
||||
for subobject in &mut object.subobjects {
|
||||
let (subobject_inline_results, subobject_mem_opt_results) = optimize_object_tree(subobject);
|
||||
inline_results += subobject_inline_results;
|
||||
mem_opt_results += subobject_mem_opt_results;
|
||||
}
|
||||
|
||||
(inline_results, mem_opt_results)
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
//! Mapping-access outlining pass for newyork IR.
|
||||
//!
|
||||
//! This pass detects multi-statement patterns in the IR and replaces them
|
||||
//! with compound IR nodes that get lowered to single outlined function calls.
|
||||
//! Runs after simplification and before LLVM codegen.
|
||||
//!
|
||||
//! Detected patterns:
|
||||
//! - Mapping SLoad: `let hash = keccak256_pair(key, slot); let value = sload(hash)`
|
||||
//! → `let value = mapping_sload(key, slot)`
|
||||
//! - Mapping SStore: `let hash = keccak256_pair(key, slot); sstore(hash, value)`
|
||||
//! → `mapping_sstore(key, slot, value)`
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::ir::{Block, Expression, Object, Region, Statement, Value};
|
||||
|
||||
/// Statistics from the compound outlining pass.
|
||||
#[derive(Default, Debug)]
|
||||
pub struct MappingAccessOutliningStatistics {
|
||||
/// Number of mapping sload patterns replaced.
|
||||
pub mapping_sloads: usize,
|
||||
/// Number of mapping sstore patterns replaced.
|
||||
pub mapping_sstores: usize,
|
||||
}
|
||||
|
||||
/// Run compound outlining on an entire object tree (including subobjects).
|
||||
pub fn outline_mapping_accesses_in_object(object: &mut Object) -> MappingAccessOutliningStatistics {
|
||||
let mut statistics = MappingAccessOutliningStatistics::default();
|
||||
|
||||
outline_block(&mut object.code, &mut statistics);
|
||||
for function in object.functions.values_mut() {
|
||||
outline_block(&mut function.body, &mut statistics);
|
||||
}
|
||||
|
||||
for sub_object in &mut object.subobjects {
|
||||
let sub_object_statistics = outline_mapping_accesses_in_object(sub_object);
|
||||
statistics.mapping_sloads += sub_object_statistics.mapping_sloads;
|
||||
statistics.mapping_sstores += sub_object_statistics.mapping_sstores;
|
||||
}
|
||||
|
||||
statistics
|
||||
}
|
||||
|
||||
/// Process a block: detect and replace compound patterns.
|
||||
///
|
||||
/// Use counts are computed once over the WHOLE block (recursively, including every nested region)
|
||||
/// and threaded into each per-list pass. A per-list count is unsound for fusion:
|
||||
/// `Statement::Block` is scope-transparent — values defined inside it stay visible to the
|
||||
/// enclosing scope (see `validate`) — so a keccak hash defined and `sload`ed inside a block but
|
||||
/// also referenced by an ancestor statement looks single-use within the block's list. Fusing it
|
||||
/// would delete the keccak definition while the outer reference still points at it, producing a
|
||||
/// use-before-definition that the IR validator rejects (a compiler ICE on valid input). The
|
||||
/// whole-block count sees the outer use and leaves such hashes alone.
|
||||
fn outline_block(block: &mut Block, statistics: &mut MappingAccessOutliningStatistics) {
|
||||
let use_counts = count_value_uses(&block.statements);
|
||||
for statement in &mut block.statements {
|
||||
outline_nested_regions(statement, &use_counts, statistics);
|
||||
}
|
||||
outline_statements(&mut block.statements, &use_counts, statistics);
|
||||
}
|
||||
|
||||
/// Recurse into nested regions within a statement.
|
||||
fn outline_nested_regions(
|
||||
statement: &mut Statement,
|
||||
use_counts: &BTreeMap<u32, usize>,
|
||||
statistics: &mut MappingAccessOutliningStatistics,
|
||||
) {
|
||||
match statement {
|
||||
Statement::If {
|
||||
then_region,
|
||||
else_region,
|
||||
..
|
||||
} => {
|
||||
outline_region(then_region, use_counts, statistics);
|
||||
if let Some(region) = else_region {
|
||||
outline_region(region, use_counts, statistics);
|
||||
}
|
||||
}
|
||||
Statement::Switch { cases, default, .. } => {
|
||||
for case in cases {
|
||||
outline_region(&mut case.body, use_counts, statistics);
|
||||
}
|
||||
if let Some(default_region) = default {
|
||||
outline_region(default_region, use_counts, statistics);
|
||||
}
|
||||
}
|
||||
Statement::For {
|
||||
condition_statements,
|
||||
body,
|
||||
post,
|
||||
..
|
||||
} => {
|
||||
for statement in condition_statements.iter_mut() {
|
||||
outline_nested_regions(statement, use_counts, statistics);
|
||||
}
|
||||
outline_region(body, use_counts, statistics);
|
||||
outline_region(post, use_counts, statistics);
|
||||
}
|
||||
Statement::Block(region) => {
|
||||
outline_region(region, use_counts, statistics);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Process a region.
|
||||
fn outline_region(
|
||||
region: &mut Region,
|
||||
use_counts: &BTreeMap<u32, usize>,
|
||||
statistics: &mut MappingAccessOutliningStatistics,
|
||||
) {
|
||||
for statement in &mut region.statements {
|
||||
outline_nested_regions(statement, use_counts, statistics);
|
||||
}
|
||||
outline_statements(&mut region.statements, use_counts, statistics);
|
||||
}
|
||||
|
||||
/// Count how many times each ValueId is referenced (used) in a statement list,
|
||||
/// recursing through nested regions and counting yields.
|
||||
///
|
||||
/// Call this on the whole enclosing block, not an inner list: fusion deletes a hash's
|
||||
/// definition, which is only sound when the hash has no uses anywhere else, and scope-transparent
|
||||
/// `Statement::Block`s let inner definitions be used by ancestor statements.
|
||||
fn count_value_uses(statements: &[Statement]) -> BTreeMap<u32, usize> {
|
||||
let mut counts = BTreeMap::new();
|
||||
for statement in statements {
|
||||
statement.for_each_value_id(&mut |id| {
|
||||
*counts.entry(id.0).or_insert(0) += 1;
|
||||
});
|
||||
}
|
||||
counts
|
||||
}
|
||||
|
||||
/// Core transformation: scan a statement list for compound patterns and replace them.
|
||||
///
|
||||
/// Detects two patterns:
|
||||
/// 1. MappingSLoad: `let hash = keccak256_pair(key, slot); ... let value = sload(hash)`
|
||||
/// where hash has exactly one use (the sload).
|
||||
/// 2. MappingSStore: `let hash = keccak256_pair(key, slot); ... sstore(hash, value)`
|
||||
/// where hash has exactly one use (the sstore).
|
||||
fn outline_statements(
|
||||
statements: &mut Vec<Statement>,
|
||||
use_counts: &BTreeMap<u32, usize>,
|
||||
statistics: &mut MappingAccessOutliningStatistics,
|
||||
) {
|
||||
let mut keccak_definitions: BTreeMap<u32, (usize, Value, Value)> = BTreeMap::new();
|
||||
for (index, statement) in statements.iter().enumerate() {
|
||||
if let Statement::Let {
|
||||
bindings,
|
||||
value: Expression::Keccak256Pair { word0, word1 },
|
||||
} = statement
|
||||
{
|
||||
if bindings.len() == 1 {
|
||||
keccak_definitions.insert(bindings[0].0, (index, *word0, *word1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if keccak_definitions.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut transformations: Vec<(usize, Statement, usize)> = Vec::new();
|
||||
|
||||
for (index, statement) in statements.iter().enumerate() {
|
||||
match statement {
|
||||
Statement::Let {
|
||||
bindings,
|
||||
value: Expression::SLoad { key, .. },
|
||||
} if bindings.len() == 1 => {
|
||||
if let Some((definition_index, word0, word1)) = keccak_definitions.get(&key.id.0) {
|
||||
if use_counts.get(&key.id.0).copied().unwrap_or(0) == 1 {
|
||||
transformations.push((
|
||||
index,
|
||||
Statement::Let {
|
||||
bindings: bindings.clone(),
|
||||
value: Expression::MappingSLoad {
|
||||
key: *word0,
|
||||
slot: *word1,
|
||||
},
|
||||
},
|
||||
*definition_index,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Statement::SStore { key, value, .. } => {
|
||||
if let Some((definition_index, word0, word1)) = keccak_definitions.get(&key.id.0) {
|
||||
if use_counts.get(&key.id.0).copied().unwrap_or(0) == 1 {
|
||||
transformations.push((
|
||||
index,
|
||||
Statement::MappingSStore {
|
||||
key: *word0,
|
||||
slot: *word1,
|
||||
value: *value,
|
||||
},
|
||||
*definition_index,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if transformations.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut indices_to_remove = std::collections::BTreeSet::new();
|
||||
let mut replacements: BTreeMap<usize, Statement> = BTreeMap::new();
|
||||
|
||||
for (statement_index, new_statement, definition_index) in transformations {
|
||||
if !indices_to_remove.contains(&definition_index)
|
||||
&& !indices_to_remove.contains(&statement_index)
|
||||
{
|
||||
indices_to_remove.insert(definition_index);
|
||||
replacements.insert(statement_index, new_statement);
|
||||
match &replacements[&statement_index] {
|
||||
Statement::Let {
|
||||
value: Expression::MappingSLoad { .. },
|
||||
..
|
||||
} => statistics.mapping_sloads += 1,
|
||||
Statement::MappingSStore { .. } => statistics.mapping_sstores += 1,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (index, new_statement) in &replacements {
|
||||
statements[*index] = new_statement.clone();
|
||||
}
|
||||
|
||||
for index in indices_to_remove.into_iter().rev() {
|
||||
statements.remove(index);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ir::{BitWidth, Type};
|
||||
use num::BigUint;
|
||||
|
||||
fn value(id: u32) -> Value {
|
||||
Value {
|
||||
id: crate::ir::ValueId(id),
|
||||
value_type: Type::Int(BitWidth::I256),
|
||||
}
|
||||
}
|
||||
|
||||
fn literal(id: u32, constant: u32) -> Statement {
|
||||
Statement::Let {
|
||||
bindings: vec![crate::ir::ValueId(id)],
|
||||
value: Expression::Literal {
|
||||
value: BigUint::from(constant),
|
||||
value_type: Type::Int(BitWidth::I256),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// A keccak hash defined inside a scope-transparent `Statement::Block` and `sload`ed there,
|
||||
/// but *also* referenced by an ancestor statement, must NOT be fused: deleting its definition
|
||||
/// would dangle the outer reference. Regression for the per-list use-count bug.
|
||||
///
|
||||
/// Value layout: word0 = v1, word1 = v2, stored value = v3, hash = v4, loaded value = v5.
|
||||
/// The outer `SStore` on v4 is an ancestor use of the block-defined hash that keeps it live.
|
||||
#[test]
|
||||
fn block_leaked_hash_not_fused() {
|
||||
let inner = Region {
|
||||
statements: vec![
|
||||
Statement::Let {
|
||||
bindings: vec![crate::ir::ValueId(4)],
|
||||
value: Expression::Keccak256Pair {
|
||||
word0: value(1),
|
||||
word1: value(2),
|
||||
},
|
||||
},
|
||||
Statement::Let {
|
||||
bindings: vec![crate::ir::ValueId(5)],
|
||||
value: Expression::SLoad {
|
||||
key: value(4),
|
||||
static_slot: None,
|
||||
},
|
||||
},
|
||||
],
|
||||
yields: vec![],
|
||||
};
|
||||
let mut object = Object {
|
||||
name: "test".to_string(),
|
||||
code: Block {
|
||||
statements: vec![
|
||||
literal(1, 0),
|
||||
literal(2, 1),
|
||||
literal(3, 42),
|
||||
Statement::Block(inner),
|
||||
Statement::SStore {
|
||||
key: value(4),
|
||||
value: value(3),
|
||||
static_slot: None,
|
||||
},
|
||||
],
|
||||
},
|
||||
functions: BTreeMap::new(),
|
||||
subobjects: vec![],
|
||||
data: BTreeMap::new(),
|
||||
};
|
||||
|
||||
let statistics = outline_mapping_accesses_in_object(&mut object);
|
||||
|
||||
assert_eq!(
|
||||
statistics.mapping_sloads, 0,
|
||||
"hash used outside the block must not be fused"
|
||||
);
|
||||
assert!(
|
||||
crate::validate::validate_object(&object).is_ok(),
|
||||
"outlining must not leave a dangling reference: {:?}",
|
||||
crate::validate::validate_object(&object)
|
||||
);
|
||||
}
|
||||
|
||||
/// Control case: a hash defined and used exactly once (no escaping use) is still fused, so the
|
||||
/// whole-block count does not regress legitimate outlining.
|
||||
#[test]
|
||||
fn single_use_hash_in_block_still_fused() {
|
||||
let inner = Region {
|
||||
statements: vec![
|
||||
Statement::Let {
|
||||
bindings: vec![crate::ir::ValueId(4)],
|
||||
value: Expression::Keccak256Pair {
|
||||
word0: value(1),
|
||||
word1: value(2),
|
||||
},
|
||||
},
|
||||
Statement::Let {
|
||||
bindings: vec![crate::ir::ValueId(5)],
|
||||
value: Expression::SLoad {
|
||||
key: value(4),
|
||||
static_slot: None,
|
||||
},
|
||||
},
|
||||
],
|
||||
yields: vec![],
|
||||
};
|
||||
let mut object = Object {
|
||||
name: "test".to_string(),
|
||||
code: Block {
|
||||
statements: vec![literal(1, 0), literal(2, 1), Statement::Block(inner)],
|
||||
},
|
||||
functions: BTreeMap::new(),
|
||||
subobjects: vec![],
|
||||
data: BTreeMap::new(),
|
||||
};
|
||||
|
||||
let statistics = outline_mapping_accesses_in_object(&mut object);
|
||||
|
||||
assert_eq!(
|
||||
statistics.mapping_sloads, 1,
|
||||
"a single-use hash should still be fused into a mapping_sload"
|
||||
);
|
||||
assert!(crate::validate::validate_object(&object).is_ok());
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,156 @@
|
||||
//! SSA conversion utilities for the newyork IR.
|
||||
//!
|
||||
//! This module provides the SSA builder that allocates fresh value IDs and tracks
|
||||
//! variable definitions across lexical scopes. Control-flow joins — choosing which
|
||||
//! values cross `if`/`for`/`switch` boundaries — are wired up by the translator in
|
||||
//! `from_yul.rs`, and the corresponding PHI nodes are emitted only later, in the
|
||||
//! LLVM IR (`to_llvm.rs`); this module does not create PHI nodes itself.
|
||||
|
||||
use crate::ir::{Value, ValueId};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// SSA builder that tracks variable definitions and creates fresh value IDs.
|
||||
#[derive(Debug)]
|
||||
pub struct SsaBuilder {
|
||||
/// Next value ID to allocate.
|
||||
next_value_id: ValueId,
|
||||
/// Current scope's variable bindings: variable name -> SSA value.
|
||||
current_scope: BTreeMap<String, Value>,
|
||||
/// Stack of saved scopes for nested blocks.
|
||||
scope_stack: Vec<BTreeMap<String, Value>>,
|
||||
}
|
||||
|
||||
impl Default for SsaBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl SsaBuilder {
|
||||
/// Creates a new SSA builder.
|
||||
pub fn new() -> Self {
|
||||
SsaBuilder {
|
||||
next_value_id: ValueId(0),
|
||||
current_scope: BTreeMap::new(),
|
||||
scope_stack: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Allocates a fresh value ID.
|
||||
pub fn fresh_id(&mut self) -> ValueId {
|
||||
self.next_value_id.fresh()
|
||||
}
|
||||
|
||||
/// Declares a new variable in the current scope.
|
||||
///
|
||||
/// Panics if a binding for `name` already exists in the current scope.
|
||||
pub fn declare(&mut self, name: &str, value: Value) {
|
||||
assert!(
|
||||
!self.current_scope.contains_key(name),
|
||||
"ICE: SsaBuilder::declare called for already-declared variable `{name}`",
|
||||
);
|
||||
self.current_scope.insert(name.to_string(), value);
|
||||
}
|
||||
|
||||
/// Assigns a new value to an existing variable in the current scope.
|
||||
///
|
||||
/// Panics if no binding for `name` exists in the current scope.
|
||||
pub fn assign(&mut self, name: &str, value: Value) {
|
||||
assert!(
|
||||
self.current_scope.contains_key(name),
|
||||
"ICE: SsaBuilder::assign called for undeclared variable `{name}`",
|
||||
);
|
||||
self.current_scope.insert(name.to_string(), value);
|
||||
}
|
||||
|
||||
/// Looks up a variable by name, returning its current SSA value.
|
||||
pub fn lookup(&self, name: &str) -> Option<Value> {
|
||||
self.current_scope.get(name).copied()
|
||||
}
|
||||
|
||||
/// Enters a new scope, saving the current scope.
|
||||
pub fn enter_scope(&mut self) {
|
||||
self.scope_stack.push(self.current_scope.clone());
|
||||
}
|
||||
|
||||
/// Exits the current scope, restoring the previous scope.
|
||||
/// Returns the scope that was exited (for computing modified variables).
|
||||
pub fn exit_scope(&mut self) -> BTreeMap<String, Value> {
|
||||
let exited = std::mem::take(&mut self.current_scope);
|
||||
self.current_scope = self.scope_stack.pop().unwrap_or_else(|| {
|
||||
panic!(
|
||||
"ICE: SsaBuilder::exit_scope called without a matching enter_scope; \
|
||||
{} binding(s) about to be silently dropped: {:?}",
|
||||
exited.len(),
|
||||
exited.keys().collect::<Vec<_>>(),
|
||||
)
|
||||
});
|
||||
exited
|
||||
}
|
||||
|
||||
/// Gets the current scope (for computing modified variables).
|
||||
pub fn current_scope(&self) -> &BTreeMap<String, Value> {
|
||||
&self.current_scope
|
||||
}
|
||||
|
||||
/// Restores scope from a saved state, used after control flow constructs.
|
||||
pub fn restore_scope(&mut self, scope: BTreeMap<String, Value>) {
|
||||
self.current_scope = scope;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ir::{BitWidth, Type};
|
||||
|
||||
#[test]
|
||||
fn test_fresh_values() {
|
||||
let mut builder = SsaBuilder::new();
|
||||
let first_id = builder.fresh_id();
|
||||
let second_id = builder.fresh_id();
|
||||
assert_eq!(first_id.0, 0);
|
||||
assert_eq!(second_id.0, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_declare_and_lookup() {
|
||||
let mut builder = SsaBuilder::new();
|
||||
let value = Value::new(builder.fresh_id(), Type::Int(BitWidth::I256));
|
||||
builder.declare("x", value);
|
||||
assert_eq!(builder.lookup("x"), Some(value));
|
||||
assert_eq!(builder.lookup("y"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nested_scopes() {
|
||||
let mut builder = SsaBuilder::new();
|
||||
let outer_value = Value::new(builder.fresh_id(), Type::Int(BitWidth::I256));
|
||||
builder.declare("x", outer_value);
|
||||
|
||||
builder.enter_scope();
|
||||
let inner_value = Value::new(builder.fresh_id(), Type::Int(BitWidth::I256));
|
||||
builder.assign("x", inner_value);
|
||||
assert_eq!(builder.lookup("x"), Some(inner_value));
|
||||
|
||||
builder.exit_scope();
|
||||
assert_eq!(builder.lookup("x"), Some(outer_value));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "ICE: SsaBuilder::declare called for already-declared variable")]
|
||||
fn declare_twice_panics() {
|
||||
let mut builder = SsaBuilder::new();
|
||||
let value = Value::new(builder.fresh_id(), Type::Int(BitWidth::I256));
|
||||
builder.declare("x", value);
|
||||
builder.declare("x", value);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "ICE: SsaBuilder::assign called for undeclared variable")]
|
||||
fn assign_undeclared_panics() {
|
||||
let mut builder = SsaBuilder::new();
|
||||
let value = Value::new(builder.fresh_id(), Type::Int(BitWidth::I256));
|
||||
builder.assign("x", value);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,887 @@
|
||||
//! IR validation passes for the newyork IR.
|
||||
//!
|
||||
//! This module provides validation passes to verify IR correctness:
|
||||
//! - SSA well-formedness: All uses are dominated by definitions
|
||||
//! - Type consistency: Operations have correctly typed operands
|
||||
//! - Region validity: All regions have correct yields matching outputs
|
||||
//!
|
||||
//! # Usage
|
||||
//!
|
||||
//! ```ignore
|
||||
//! use revive_newyork::validate::{validate_object, ValidationError};
|
||||
//!
|
||||
//! let result = validate_object(&ir_object);
|
||||
//! if let Err(errors) = result {
|
||||
//! for error in errors {
|
||||
//! eprintln!("Validation error: {}", error);
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
use crate::ir::{Block, Expression, Function, Object, Region, Statement, Value, ValueId};
|
||||
use std::collections::BTreeSet;
|
||||
use thiserror::Error;
|
||||
|
||||
/// Validation error types.
|
||||
#[derive(Error, Debug, Clone)]
|
||||
pub enum ValidationError {
|
||||
/// A value is used before it is defined.
|
||||
#[error("SSA error: value v{0} used before definition at {1}")]
|
||||
UseBeforeDef(u32, String),
|
||||
|
||||
/// A value is defined multiple times.
|
||||
#[error("SSA error: value v{0} defined multiple times")]
|
||||
MultipleDef(u32),
|
||||
|
||||
/// Region yields wrong number of values.
|
||||
#[error("Region error: expected {expected} yields, got {actual} at {location}")]
|
||||
YieldCountMismatch {
|
||||
expected: usize,
|
||||
actual: usize,
|
||||
location: String,
|
||||
},
|
||||
|
||||
/// Function has inconsistent return value count.
|
||||
#[error("Function error: {0}")]
|
||||
FunctionError(String),
|
||||
|
||||
/// Unknown function called.
|
||||
#[error("Unknown function: f{0}")]
|
||||
UnknownFunction(u32),
|
||||
}
|
||||
|
||||
/// Result of validation.
|
||||
pub type ValidationResult = Result<(), Vec<ValidationError>>;
|
||||
|
||||
/// Validates an IR object, returning all errors found.
|
||||
pub fn validate_object(object: &Object) -> ValidationResult {
|
||||
let mut validator = Validator::new();
|
||||
validator.validate_object(object);
|
||||
validator.into_result()
|
||||
}
|
||||
|
||||
/// Internal validator state.
|
||||
struct Validator {
|
||||
/// Set of defined value IDs (visible in current scope).
|
||||
defined: BTreeSet<u32>,
|
||||
/// Stack of defined sets for scope management.
|
||||
scope_stack: Vec<BTreeSet<u32>>,
|
||||
/// Set of known function IDs.
|
||||
known_functions: BTreeSet<u32>,
|
||||
/// Collected errors.
|
||||
errors: Vec<ValidationError>,
|
||||
}
|
||||
|
||||
impl Validator {
|
||||
fn new() -> Self {
|
||||
Validator {
|
||||
defined: BTreeSet::new(),
|
||||
scope_stack: Vec::new(),
|
||||
known_functions: BTreeSet::new(),
|
||||
errors: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn into_result(self) -> ValidationResult {
|
||||
if self.errors.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(self.errors)
|
||||
}
|
||||
}
|
||||
|
||||
fn error(&mut self, validation_error: ValidationError) {
|
||||
self.errors.push(validation_error);
|
||||
}
|
||||
|
||||
fn enter_scope(&mut self) {
|
||||
self.scope_stack.push(self.defined.clone());
|
||||
}
|
||||
|
||||
fn exit_scope(&mut self) {
|
||||
if let Some(saved) = self.scope_stack.pop() {
|
||||
self.defined = saved;
|
||||
}
|
||||
}
|
||||
|
||||
fn define(&mut self, id: ValueId) {
|
||||
if !self.defined.insert(id.0) {
|
||||
self.error(ValidationError::MultipleDef(id.0));
|
||||
}
|
||||
}
|
||||
|
||||
fn use_value(&mut self, value: &Value, context: &str) {
|
||||
self.use_value_id(value.id, context);
|
||||
}
|
||||
|
||||
fn use_value_id(&mut self, id: ValueId, context: &str) {
|
||||
if !self.defined.contains(&id.0) {
|
||||
self.error(ValidationError::UseBeforeDef(id.0, context.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Subobjects compile to separate units with their own SSA ID and function-ID
|
||||
/// namespaces — they share no values or callables with the parent. Each is validated
|
||||
/// with a fresh validator so the parent's defined IDs don't leak in and produce
|
||||
/// spurious MultipleDef errors when the subobject reuses the same value numbers.
|
||||
fn validate_object(&mut self, object: &Object) {
|
||||
for id in object.functions.keys() {
|
||||
self.known_functions.insert(id.0);
|
||||
}
|
||||
|
||||
self.validate_block(&object.code, &format!("object '{}' code", object.name));
|
||||
|
||||
for function in object.functions.values() {
|
||||
self.validate_function(function);
|
||||
}
|
||||
|
||||
for subobject in &object.subobjects {
|
||||
if let Err(sub_errors) = validate_object(subobject) {
|
||||
self.errors.extend(sub_errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_function(&mut self, function: &Function) {
|
||||
self.enter_scope();
|
||||
|
||||
for (id, _ty) in &function.parameters {
|
||||
self.define(*id);
|
||||
}
|
||||
|
||||
for id in &function.return_values_initial {
|
||||
self.define(*id);
|
||||
}
|
||||
|
||||
if function.returns.len() != function.return_values_initial.len() {
|
||||
self.error(ValidationError::FunctionError(format!(
|
||||
"function '{}': returns count ({}) != return_values_initial count ({})",
|
||||
function.name,
|
||||
function.returns.len(),
|
||||
function.return_values_initial.len()
|
||||
)));
|
||||
}
|
||||
|
||||
if function.returns.len() != function.return_values.len() {
|
||||
self.error(ValidationError::FunctionError(format!(
|
||||
"function '{}': returns count ({}) != return_values count ({})",
|
||||
function.name,
|
||||
function.returns.len(),
|
||||
function.return_values.len()
|
||||
)));
|
||||
}
|
||||
|
||||
self.validate_block(&function.body, &format!("function '{}'", function.name));
|
||||
|
||||
for id in &function.return_values {
|
||||
self.use_value_id(*id, &format!("function '{}' return", function.name));
|
||||
}
|
||||
|
||||
self.exit_scope();
|
||||
}
|
||||
|
||||
fn validate_block(&mut self, block: &Block, context: &str) {
|
||||
for (statement_index, statement) in block.statements.iter().enumerate() {
|
||||
self.validate_statement(statement, &format!("{}[{}]", context, statement_index));
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_region(&mut self, region: &Region, context: &str) {
|
||||
for (statement_index, statement) in region.statements.iter().enumerate() {
|
||||
self.validate_statement(statement, &format!("{}[{}]", context, statement_index));
|
||||
}
|
||||
|
||||
for value in ®ion.yields {
|
||||
self.use_value(value, &format!("{} yield", context));
|
||||
}
|
||||
}
|
||||
|
||||
/// Scope-isolation and yield-count checks mirror the codegen's control-flow shape:
|
||||
///
|
||||
/// - For a `Statement::Switch` with no explicit default, the unmatched-scrutinee path
|
||||
/// implicitly yields `inputs` unchanged (mirroring a `Statement::If` without an else
|
||||
/// region), so the outputs must line up with the inputs.
|
||||
/// - A standalone `Statement::Block` is straight-line code in the codegen (no LLVM-level
|
||||
/// scoping, no phi merge — `generate_region` just lowers the statements). SSA values
|
||||
/// defined inside flow through to the surrounding scope, matching
|
||||
/// `Statement::For`/`If`/`Switch` which DO scope-isolate because their regions form
|
||||
/// control-flow splits.
|
||||
fn validate_statement(&mut self, statement: &Statement, context: &str) {
|
||||
match statement {
|
||||
Statement::Let { bindings, value } => {
|
||||
self.validate_expression(value, context);
|
||||
|
||||
for id in bindings {
|
||||
self.define(*id);
|
||||
}
|
||||
}
|
||||
|
||||
Statement::MStore { offset, value, .. } => {
|
||||
self.use_value(offset, context);
|
||||
self.use_value(value, context);
|
||||
}
|
||||
|
||||
Statement::MStore8 { offset, value, .. } => {
|
||||
self.use_value(offset, context);
|
||||
self.use_value(value, context);
|
||||
}
|
||||
|
||||
Statement::MCopy {
|
||||
destination,
|
||||
source,
|
||||
length,
|
||||
} => {
|
||||
self.use_value(destination, context);
|
||||
self.use_value(source, context);
|
||||
self.use_value(length, context);
|
||||
}
|
||||
|
||||
Statement::SStore { key, value, .. } => {
|
||||
self.use_value(key, context);
|
||||
self.use_value(value, context);
|
||||
}
|
||||
|
||||
Statement::TStore { key, value } => {
|
||||
self.use_value(key, context);
|
||||
self.use_value(value, context);
|
||||
}
|
||||
|
||||
Statement::MappingSStore { key, slot, value } => {
|
||||
self.use_value(key, context);
|
||||
self.use_value(slot, context);
|
||||
self.use_value(value, context);
|
||||
}
|
||||
|
||||
Statement::If {
|
||||
condition,
|
||||
inputs,
|
||||
then_region,
|
||||
else_region,
|
||||
outputs,
|
||||
} => {
|
||||
self.use_value(condition, context);
|
||||
for input in inputs {
|
||||
self.use_value(input, context);
|
||||
}
|
||||
|
||||
self.enter_scope();
|
||||
self.validate_region(then_region, &format!("{} then", context));
|
||||
let then_yield_count = then_region.yields.len();
|
||||
self.exit_scope();
|
||||
|
||||
let else_yield_count = if let Some(else_region) = else_region {
|
||||
self.enter_scope();
|
||||
self.validate_region(else_region, &format!("{} else", context));
|
||||
let count = else_region.yields.len();
|
||||
self.exit_scope();
|
||||
count
|
||||
} else {
|
||||
inputs.len()
|
||||
};
|
||||
|
||||
if outputs.len() != then_yield_count {
|
||||
self.error(ValidationError::YieldCountMismatch {
|
||||
expected: outputs.len(),
|
||||
actual: then_yield_count,
|
||||
location: format!("{} then", context),
|
||||
});
|
||||
}
|
||||
|
||||
if outputs.len() != else_yield_count {
|
||||
self.error(ValidationError::YieldCountMismatch {
|
||||
expected: outputs.len(),
|
||||
actual: else_yield_count,
|
||||
location: format!("{} else", context),
|
||||
});
|
||||
}
|
||||
|
||||
for id in outputs {
|
||||
self.define(*id);
|
||||
}
|
||||
}
|
||||
|
||||
Statement::Switch {
|
||||
scrutinee,
|
||||
inputs,
|
||||
cases,
|
||||
default,
|
||||
outputs,
|
||||
} => {
|
||||
self.use_value(scrutinee, context);
|
||||
for input in inputs {
|
||||
self.use_value(input, context);
|
||||
}
|
||||
|
||||
for (case_index, case) in cases.iter().enumerate() {
|
||||
self.enter_scope();
|
||||
self.validate_region(&case.body, &format!("{} case[{}]", context, case_index));
|
||||
|
||||
if outputs.len() != case.body.yields.len() {
|
||||
self.error(ValidationError::YieldCountMismatch {
|
||||
expected: outputs.len(),
|
||||
actual: case.body.yields.len(),
|
||||
location: format!("{} case[{}]", context, case_index),
|
||||
});
|
||||
}
|
||||
self.exit_scope();
|
||||
}
|
||||
|
||||
if let Some(default_region) = default {
|
||||
self.enter_scope();
|
||||
self.validate_region(default_region, &format!("{} default", context));
|
||||
|
||||
if outputs.len() != default_region.yields.len() {
|
||||
self.error(ValidationError::YieldCountMismatch {
|
||||
expected: outputs.len(),
|
||||
actual: default_region.yields.len(),
|
||||
location: format!("{} default", context),
|
||||
});
|
||||
}
|
||||
self.exit_scope();
|
||||
} else if outputs.len() != inputs.len() {
|
||||
self.error(ValidationError::YieldCountMismatch {
|
||||
expected: outputs.len(),
|
||||
actual: inputs.len(),
|
||||
location: format!("{} default", context),
|
||||
});
|
||||
}
|
||||
|
||||
for id in outputs {
|
||||
self.define(*id);
|
||||
}
|
||||
}
|
||||
|
||||
Statement::For {
|
||||
initial_values,
|
||||
loop_variables,
|
||||
condition_statements,
|
||||
condition,
|
||||
body,
|
||||
post_input_variables,
|
||||
post,
|
||||
outputs,
|
||||
} => {
|
||||
for initial_value in initial_values {
|
||||
self.use_value(initial_value, context);
|
||||
}
|
||||
|
||||
if loop_variables.len() != initial_values.len() {
|
||||
self.error(ValidationError::FunctionError(format!(
|
||||
"{}: loop_variables count ({}) != initial_values count ({})",
|
||||
context,
|
||||
loop_variables.len(),
|
||||
initial_values.len()
|
||||
)));
|
||||
}
|
||||
|
||||
if post_input_variables.len() != loop_variables.len() {
|
||||
self.error(ValidationError::FunctionError(format!(
|
||||
"{}: post_input_variables count ({}) != loop_variables count ({})",
|
||||
context,
|
||||
post_input_variables.len(),
|
||||
loop_variables.len()
|
||||
)));
|
||||
}
|
||||
|
||||
self.enter_scope();
|
||||
for id in loop_variables {
|
||||
self.define(*id);
|
||||
}
|
||||
|
||||
for (condition_statement_index, statement) in
|
||||
condition_statements.iter().enumerate()
|
||||
{
|
||||
self.validate_statement(
|
||||
statement,
|
||||
&format!("{} cond_stmt[{}]", context, condition_statement_index),
|
||||
);
|
||||
}
|
||||
|
||||
self.validate_expression(condition, &format!("{} condition", context));
|
||||
|
||||
self.validate_region(body, &format!("{} body", context));
|
||||
|
||||
if loop_variables.len() != body.yields.len() {
|
||||
self.error(ValidationError::YieldCountMismatch {
|
||||
expected: loop_variables.len(),
|
||||
actual: body.yields.len(),
|
||||
location: format!("{} body", context),
|
||||
});
|
||||
}
|
||||
|
||||
for id in post_input_variables {
|
||||
self.define(*id);
|
||||
}
|
||||
|
||||
self.validate_region(post, &format!("{} post", context));
|
||||
|
||||
if loop_variables.len() != post.yields.len() {
|
||||
self.error(ValidationError::YieldCountMismatch {
|
||||
expected: loop_variables.len(),
|
||||
actual: post.yields.len(),
|
||||
location: format!("{} post", context),
|
||||
});
|
||||
}
|
||||
|
||||
self.exit_scope();
|
||||
|
||||
if outputs.len() != loop_variables.len() {
|
||||
self.error(ValidationError::FunctionError(format!(
|
||||
"{}: outputs count ({}) != loop_variables count ({})",
|
||||
context,
|
||||
outputs.len(),
|
||||
loop_variables.len()
|
||||
)));
|
||||
}
|
||||
|
||||
for id in outputs {
|
||||
self.define(*id);
|
||||
}
|
||||
}
|
||||
|
||||
Statement::Break { values } | Statement::Continue { values } => {
|
||||
for value in values {
|
||||
self.use_value(value, context);
|
||||
}
|
||||
}
|
||||
|
||||
Statement::Leave { return_values } => {
|
||||
for return_value in return_values {
|
||||
self.use_value(return_value, context);
|
||||
}
|
||||
}
|
||||
|
||||
Statement::Revert { offset, length } => {
|
||||
self.use_value(offset, context);
|
||||
self.use_value(length, context);
|
||||
}
|
||||
|
||||
Statement::Return { offset, length } => {
|
||||
self.use_value(offset, context);
|
||||
self.use_value(length, context);
|
||||
}
|
||||
|
||||
Statement::Stop
|
||||
| Statement::Invalid
|
||||
| Statement::PanicRevert { .. }
|
||||
| Statement::ErrorStringRevert { .. }
|
||||
| Statement::CustomErrorRevert { .. } => {}
|
||||
|
||||
Statement::SelfDestruct { address } => {
|
||||
self.use_value(address, context);
|
||||
}
|
||||
|
||||
Statement::ExternalCall {
|
||||
gas,
|
||||
address,
|
||||
value,
|
||||
args_offset,
|
||||
args_length,
|
||||
ret_offset,
|
||||
ret_length,
|
||||
result,
|
||||
..
|
||||
} => {
|
||||
self.use_value(gas, context);
|
||||
self.use_value(address, context);
|
||||
if let Some(call_value) = value {
|
||||
self.use_value(call_value, context);
|
||||
}
|
||||
self.use_value(args_offset, context);
|
||||
self.use_value(args_length, context);
|
||||
self.use_value(ret_offset, context);
|
||||
self.use_value(ret_length, context);
|
||||
self.define(*result);
|
||||
}
|
||||
|
||||
Statement::Create {
|
||||
value,
|
||||
offset,
|
||||
length,
|
||||
salt,
|
||||
result,
|
||||
..
|
||||
} => {
|
||||
self.use_value(value, context);
|
||||
self.use_value(offset, context);
|
||||
self.use_value(length, context);
|
||||
if let Some(salt) = salt {
|
||||
self.use_value(salt, context);
|
||||
}
|
||||
self.define(*result);
|
||||
}
|
||||
|
||||
Statement::Log {
|
||||
offset,
|
||||
length,
|
||||
topics,
|
||||
} => {
|
||||
self.use_value(offset, context);
|
||||
self.use_value(length, context);
|
||||
for topic in topics {
|
||||
self.use_value(topic, context);
|
||||
}
|
||||
}
|
||||
|
||||
Statement::CodeCopy {
|
||||
destination,
|
||||
offset,
|
||||
length,
|
||||
} => {
|
||||
self.use_value(destination, context);
|
||||
self.use_value(offset, context);
|
||||
self.use_value(length, context);
|
||||
}
|
||||
|
||||
Statement::ExtCodeCopy {
|
||||
address,
|
||||
destination,
|
||||
offset,
|
||||
length,
|
||||
} => {
|
||||
self.use_value(address, context);
|
||||
self.use_value(destination, context);
|
||||
self.use_value(offset, context);
|
||||
self.use_value(length, context);
|
||||
}
|
||||
|
||||
Statement::ReturnDataCopy {
|
||||
destination,
|
||||
offset,
|
||||
length,
|
||||
} => {
|
||||
self.use_value(destination, context);
|
||||
self.use_value(offset, context);
|
||||
self.use_value(length, context);
|
||||
}
|
||||
|
||||
Statement::DataCopy {
|
||||
destination,
|
||||
offset,
|
||||
length,
|
||||
} => {
|
||||
self.use_value(destination, context);
|
||||
self.use_value(offset, context);
|
||||
self.use_value(length, context);
|
||||
}
|
||||
|
||||
Statement::CallDataCopy {
|
||||
destination,
|
||||
offset,
|
||||
length,
|
||||
} => {
|
||||
self.use_value(destination, context);
|
||||
self.use_value(offset, context);
|
||||
self.use_value(length, context);
|
||||
}
|
||||
|
||||
Statement::Block(region) => {
|
||||
self.validate_region(region, context);
|
||||
}
|
||||
|
||||
Statement::Expression(expression) => {
|
||||
self.validate_expression(expression, context);
|
||||
}
|
||||
|
||||
Statement::SetImmutable { value, .. } => {
|
||||
self.use_value(value, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_expression(&mut self, expression: &Expression, context: &str) {
|
||||
match expression {
|
||||
Expression::Literal { .. } => {}
|
||||
|
||||
Expression::Var(id) => {
|
||||
self.use_value_id(*id, context);
|
||||
}
|
||||
|
||||
Expression::Binary { lhs, rhs, .. } => {
|
||||
self.use_value(lhs, context);
|
||||
self.use_value(rhs, context);
|
||||
}
|
||||
|
||||
Expression::Ternary { a, b, n, .. } => {
|
||||
self.use_value(a, context);
|
||||
self.use_value(b, context);
|
||||
self.use_value(n, context);
|
||||
}
|
||||
|
||||
Expression::Unary { operand, .. } => {
|
||||
self.use_value(operand, context);
|
||||
}
|
||||
|
||||
Expression::CallDataLoad { offset } => {
|
||||
self.use_value(offset, context);
|
||||
}
|
||||
|
||||
Expression::CallValue
|
||||
| Expression::Caller
|
||||
| Expression::Origin
|
||||
| Expression::CallDataSize
|
||||
| Expression::CodeSize
|
||||
| Expression::GasPrice
|
||||
| Expression::ReturnDataSize
|
||||
| Expression::Coinbase
|
||||
| Expression::Timestamp
|
||||
| Expression::Number
|
||||
| Expression::Difficulty
|
||||
| Expression::GasLimit
|
||||
| Expression::ChainId
|
||||
| Expression::SelfBalance
|
||||
| Expression::BaseFee
|
||||
| Expression::BlobBaseFee
|
||||
| Expression::Gas
|
||||
| Expression::MSize
|
||||
| Expression::Address => {}
|
||||
|
||||
Expression::ExtCodeSize { address } => {
|
||||
self.use_value(address, context);
|
||||
}
|
||||
|
||||
Expression::ExtCodeHash { address } => {
|
||||
self.use_value(address, context);
|
||||
}
|
||||
|
||||
Expression::BlockHash { number } => {
|
||||
self.use_value(number, context);
|
||||
}
|
||||
|
||||
Expression::BlobHash { index } => {
|
||||
self.use_value(index, context);
|
||||
}
|
||||
|
||||
Expression::Balance { address } => {
|
||||
self.use_value(address, context);
|
||||
}
|
||||
|
||||
Expression::MLoad { offset, .. } => {
|
||||
self.use_value(offset, context);
|
||||
}
|
||||
|
||||
Expression::SLoad { key, .. } => {
|
||||
self.use_value(key, context);
|
||||
}
|
||||
|
||||
Expression::TLoad { key } => {
|
||||
self.use_value(key, context);
|
||||
}
|
||||
|
||||
Expression::Call {
|
||||
function,
|
||||
arguments,
|
||||
} => {
|
||||
if !self.known_functions.contains(&function.0) {
|
||||
self.error(ValidationError::UnknownFunction(function.0));
|
||||
}
|
||||
for argument in arguments {
|
||||
self.use_value(argument, context);
|
||||
}
|
||||
}
|
||||
|
||||
Expression::Truncate { value, .. }
|
||||
| Expression::ZeroExtend { value, .. }
|
||||
| Expression::SignExtendTo { value, .. } => {
|
||||
self.use_value(value, context);
|
||||
}
|
||||
|
||||
Expression::Keccak256 { offset, length } => {
|
||||
self.use_value(offset, context);
|
||||
self.use_value(length, context);
|
||||
}
|
||||
|
||||
Expression::Keccak256Pair { word0, word1 } => {
|
||||
self.use_value(word0, context);
|
||||
self.use_value(word1, context);
|
||||
}
|
||||
|
||||
Expression::Keccak256Single { word0 } => {
|
||||
self.use_value(word0, context);
|
||||
}
|
||||
|
||||
Expression::MappingSLoad { key, slot } => {
|
||||
self.use_value(key, context);
|
||||
self.use_value(slot, context);
|
||||
}
|
||||
|
||||
Expression::DataOffset { .. }
|
||||
| Expression::DataSize { .. }
|
||||
| Expression::LoadImmutable { .. }
|
||||
| Expression::LinkerSymbol { .. } => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ir::{
|
||||
BinaryOperation, BitWidth, Block, FunctionId, Object, Region, Statement, Type, Value,
|
||||
ValueId,
|
||||
};
|
||||
use num::BigUint;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
fn int_value(id: u32) -> Value {
|
||||
Value::int(ValueId(id))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_let() {
|
||||
let object = Object {
|
||||
name: "Test".to_string(),
|
||||
code: Block {
|
||||
statements: vec![
|
||||
Statement::Let {
|
||||
bindings: vec![ValueId(0)],
|
||||
value: Expression::Literal {
|
||||
value: BigUint::from(0u64),
|
||||
value_type: Type::Int(BitWidth::I256),
|
||||
},
|
||||
},
|
||||
Statement::Let {
|
||||
bindings: vec![ValueId(1)],
|
||||
value: Expression::Var(ValueId(0)),
|
||||
},
|
||||
],
|
||||
},
|
||||
functions: BTreeMap::new(),
|
||||
subobjects: Vec::new(),
|
||||
data: BTreeMap::new(),
|
||||
};
|
||||
assert!(validate_object(&object).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_use_before_def() {
|
||||
let object = Object {
|
||||
name: "Test".to_string(),
|
||||
code: Block {
|
||||
statements: vec![Statement::Let {
|
||||
bindings: vec![ValueId(1)],
|
||||
value: Expression::Var(ValueId(0)),
|
||||
}],
|
||||
},
|
||||
functions: BTreeMap::new(),
|
||||
subobjects: Vec::new(),
|
||||
data: BTreeMap::new(),
|
||||
};
|
||||
let result = validate_object(&object);
|
||||
assert!(result.is_err());
|
||||
let errors = result.unwrap_err();
|
||||
assert!(errors
|
||||
.iter()
|
||||
.any(|error| matches!(error, ValidationError::UseBeforeDef(0, _))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_if_yield_mismatch() {
|
||||
let object = Object {
|
||||
name: "Test".to_string(),
|
||||
code: Block {
|
||||
statements: vec![
|
||||
Statement::Let {
|
||||
bindings: vec![ValueId(0)],
|
||||
value: Expression::Literal {
|
||||
value: BigUint::from(1u64),
|
||||
value_type: Type::Int(BitWidth::I256),
|
||||
},
|
||||
},
|
||||
Statement::If {
|
||||
condition: int_value(0),
|
||||
inputs: vec![],
|
||||
then_region: Region {
|
||||
statements: vec![],
|
||||
yields: vec![int_value(0)],
|
||||
},
|
||||
else_region: Some(Region {
|
||||
statements: vec![],
|
||||
yields: vec![],
|
||||
}),
|
||||
outputs: vec![ValueId(1)],
|
||||
},
|
||||
],
|
||||
},
|
||||
functions: BTreeMap::new(),
|
||||
subobjects: Vec::new(),
|
||||
data: BTreeMap::new(),
|
||||
};
|
||||
let result = validate_object(&object);
|
||||
assert!(result.is_err());
|
||||
let errors = result.unwrap_err();
|
||||
assert!(errors
|
||||
.iter()
|
||||
.any(|error| matches!(error, ValidationError::YieldCountMismatch { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_function() {
|
||||
let mut functions = BTreeMap::new();
|
||||
functions.insert(
|
||||
FunctionId(0),
|
||||
Function {
|
||||
id: FunctionId(0),
|
||||
name: "add_one".to_string(),
|
||||
parameters: vec![(ValueId(0), Type::Int(BitWidth::I256))],
|
||||
returns: vec![Type::Int(BitWidth::I256)],
|
||||
return_values_initial: vec![ValueId(1)],
|
||||
return_values: vec![ValueId(2)],
|
||||
body: Block {
|
||||
statements: vec![Statement::Let {
|
||||
bindings: vec![ValueId(2)],
|
||||
value: Expression::Binary {
|
||||
operation: BinaryOperation::Add,
|
||||
lhs: int_value(0),
|
||||
rhs: int_value(1),
|
||||
},
|
||||
}],
|
||||
},
|
||||
call_count: 0,
|
||||
size_estimate: 0,
|
||||
},
|
||||
);
|
||||
|
||||
let object = Object {
|
||||
name: "Test".to_string(),
|
||||
code: Block { statements: vec![] },
|
||||
functions,
|
||||
subobjects: Vec::new(),
|
||||
data: BTreeMap::new(),
|
||||
};
|
||||
assert!(validate_object(&object).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_function_return_count_mismatch() {
|
||||
let mut functions = BTreeMap::new();
|
||||
functions.insert(
|
||||
FunctionId(0),
|
||||
Function {
|
||||
id: FunctionId(0),
|
||||
name: "bad".to_string(),
|
||||
parameters: vec![],
|
||||
returns: vec![Type::Int(BitWidth::I256), Type::Int(BitWidth::I256)],
|
||||
return_values_initial: vec![ValueId(0)],
|
||||
return_values: vec![ValueId(0)],
|
||||
body: Block { statements: vec![] },
|
||||
call_count: 0,
|
||||
size_estimate: 0,
|
||||
},
|
||||
);
|
||||
|
||||
let object = Object {
|
||||
name: "Test".to_string(),
|
||||
code: Block { statements: vec![] },
|
||||
functions,
|
||||
subobjects: Vec::new(),
|
||||
data: BTreeMap::new(),
|
||||
};
|
||||
let result = validate_object(&object);
|
||||
assert!(result.is_err());
|
||||
let errors = result.unwrap_err();
|
||||
assert!(errors
|
||||
.iter()
|
||||
.any(|error| matches!(error, ValidationError::FunctionError(_))));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user