From 4e4d7a60230d9f837d1a89c1e4bee1537ba619a1 Mon Sep 17 00:00:00 2001 From: Vaivaswatha N Date: Tue, 7 Feb 2023 20:25:00 +0530 Subject: [PATCH] A new pass manager (#3856) The new pass manager replaces the existing one, and also is now used in the main `forc build` pipeline (the old pass manager was restricted to the `opt` executable). It doesn't fully achieve #2399 and #3596 yet. i.e., it doesn't do automatic dependence resolution and handle categories of passes. --------- Co-authored-by: Mohammad Fawaz --- Cargo.lock | 7 + sway-core/src/lib.rs | 249 +++++---------------------- sway-core/src/metadata.rs | 6 + sway-ir/Cargo.toml | 1 + sway-ir/src/bin/opt.rs | 25 +-- sway-ir/src/optimize/constants.rs | 32 ++-- sway-ir/src/optimize/dce.rs | 47 +++-- sway-ir/src/optimize/inline.rs | 195 +++++++++++++++++++-- sway-ir/src/optimize/mem2reg.rs | 43 +++-- sway-ir/src/optimize/simplify_cfg.rs | 32 ++-- sway-ir/src/pass_manager.rs | 178 +++++++++++++++---- sway-ir/tests/tests.rs | 53 +++--- test/src/ir_generation/mod.rs | 26 +-- 13 files changed, 515 insertions(+), 379 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cb500ecbd16..5ff33470f81 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1225,6 +1225,12 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" +[[package]] +name = "downcast-rs" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" + [[package]] name = "ecdsa" version = "0.14.8" @@ -4720,6 +4726,7 @@ name = "sway-ir" version = "0.34.0" dependencies = [ "anyhow", + "downcast-rs", "filecheck", "generational-arena", "peg", diff --git a/sway-core/src/lib.rs b/sway-core/src/lib.rs index dce0ae2d4ad..560eb3129b1 100644 --- a/sway-core/src/lib.rs +++ b/sway-core/src/lib.rs @@ -18,7 +18,7 @@ pub mod transform; pub mod type_system; use crate::ir_generation::check_function_purity; -use crate::language::Inline; +use crate::language::parsed::TreeType; use crate::{error::*, source_map::SourceMap}; pub use asm_generation::from_ir::compile_ir_to_asm; use asm_generation::FinalizedAsm; @@ -30,7 +30,11 @@ use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::sync::Arc; use sway_error::handler::{ErrorEmitted, Handler}; -use sway_ir::{call_graph, Context, Function, Instruction, Kind, Module, Value}; +use sway_ir::{ + create_const_combine_pass, create_dce_pass, create_func_dce_pass, + create_inline_in_non_predicate_pass, create_inline_in_predicate_pass, create_mem2reg_pass, + create_simplify_cfg_pass, Context, Kind, Module, PassManager, PassManagerConfig, +}; pub use semantic_analysis::namespace::{self, Namespace}; pub mod types; @@ -467,41 +471,42 @@ pub(crate) fn compile_ast_to_ir_to_asm( errors.extend(e); } - // Now we're working with all functions in the module. - let all_functions = ir - .module_iter() - .flat_map(|module| module.function_iter(&ir)) - .collect::>(); - - // Promote local values to registers. - check!( - promote_to_registers(&mut ir, &all_functions), - return err(warnings, errors), - warnings, - errors - ); - - // Inline function calls. - check!( - inline_function_calls(&mut ir, &all_functions, &tree_type), - return err(warnings, errors), - warnings, - errors - ); + // Initialize the pass manager and a config for it. + let mut pass_mgr = PassManager::default(); + let mut pmgr_config = PassManagerConfig { to_run: vec![] }; + // Register required passes. + let mem2reg = pass_mgr.register(create_mem2reg_pass()); + let inline = if matches!(tree_type, TreeType::Predicate) { + pass_mgr.register(create_inline_in_predicate_pass()) + } else { + pass_mgr.register(create_inline_in_non_predicate_pass()) + }; + let const_combine = pass_mgr.register(create_const_combine_pass()); + let simplify_cfg = pass_mgr.register(create_simplify_cfg_pass()); + let func_dce = pass_mgr.register(create_func_dce_pass()); + let dce = pass_mgr.register(create_dce_pass()); + + // Configure to run our passes. + pmgr_config.to_run.push(mem2reg.to_string()); + pmgr_config.to_run.push(inline.to_string()); + pmgr_config.to_run.push(const_combine.to_string()); + pmgr_config.to_run.push(simplify_cfg.to_string()); + pmgr_config.to_run.push(const_combine.to_string()); + pmgr_config.to_run.push(simplify_cfg.to_string()); + pmgr_config.to_run.push(func_dce.to_string()); + pmgr_config.to_run.push(dce.to_string()); + + // Run the passes. let res = CompileResult::with_handler(|handler| { - // TODO: Experiment with putting combine-constants and simplify-cfg - // in a loop, but per function. - combine_constants(handler, &mut ir, &all_functions)?; - simplify_cfg(handler, &mut ir, &all_functions)?; - // Simplify-CFG helps combine constants. - combine_constants(handler, &mut ir, &all_functions)?; - // And that in-turn enables more simplify-cfg. - simplify_cfg(handler, &mut ir, &all_functions)?; - - // Remove dead definitions based on the entry points root set. - dce(handler, &mut ir, &entry_point_functions)?; - Ok(()) + if let Err(ir_error) = pass_mgr.run(&mut ir, &pmgr_config) { + Err(handler.emit_err(CompileError::InternalOwned( + ir_error.to_string(), + span::Span::dummy(), + ))) + } else { + Ok(()) + } }); check!(res, return err(warnings, errors), warnings, errors); @@ -519,180 +524,6 @@ pub(crate) fn compile_ast_to_ir_to_asm( ok(final_asm, warnings, errors) } -fn promote_to_registers(ir: &mut Context, functions: &[Function]) -> CompileResult<()> { - for function in functions { - if let Err(ir_error) = sway_ir::optimize::promote_to_registers(ir, function) { - return err( - Vec::new(), - vec![CompileError::InternalOwned( - ir_error.to_string(), - span::Span::dummy(), - )], - ); - } - } - ok((), Vec::new(), Vec::new()) -} - -/// Inline function calls based on two conditions: -/// 1. The program we're compiling is a "predicate". Predicates cannot jump backwards which means -/// that supporting function calls (i.e. without inlining) is not possible. This is a protocl -/// restriction and not a heuristic. -/// 2. If the program is not a "predicate" then, we rely on some heuristic which is described below -/// in the `inline_heuristc` closure. -/// -pub fn inline_function_calls( - ir: &mut Context, - functions: &[Function], - tree_type: &parsed::TreeType, -) -> CompileResult<()> { - // Inspect ALL calls and count how often each function is called. - // This is not required for predicates because we don't inline their function calls - let call_counts: HashMap = match tree_type { - parsed::TreeType::Predicate => HashMap::new(), - _ => functions.iter().fold(HashMap::new(), |mut counts, func| { - for (_block, ins) in func.instruction_iter(ir) { - if let Some(Instruction::Call(callee, _args)) = ins.get_instruction(ir) { - counts - .entry(*callee) - .and_modify(|count| *count += 1) - .or_insert(1); - } - } - counts - }), - }; - - let inline_heuristic = |ctx: &Context, func: &Function, _call_site: &Value| { - let mut md_mgr = metadata::MetadataManager::default(); - let attributed_inline = md_mgr.md_to_inline(ctx, func.get_metadata(ctx)); - - match attributed_inline { - Some(Inline::Always) => { - // TODO: check if inlining of function is possible - // return true; - } - Some(Inline::Never) => { - return false; - } - None => {} - } - - // For now, pending improvements to ASMgen for calls, we must inline any function which has - // too many args. - if func.args_iter(ctx).count() as u8 - > crate::asm_generation::fuel::compiler_constants::NUM_ARG_REGISTERS - { - return true; - } - - // If the function is called only once then definitely inline it. - if call_counts.get(func).copied().unwrap_or(0) == 1 { - return true; - } - - // If the function is (still) small then also inline it. - const MAX_INLINE_INSTRS_COUNT: usize = 4; - if func.num_instructions(ctx) <= MAX_INLINE_INSTRS_COUNT { - return true; - } - - // As per https://github.com/FuelLabs/sway/issues/2819 we can hit problems if a function - // argument is used as a pointer (probably because it has a ref type) although it actually - // isn't one. Ref type args which aren't pointers need to be inlined. - if func.args_iter(ctx).any(|(_name, arg_val)| { - arg_val - .get_argument_type_and_byref(ctx) - .map(|(ty, by_ref)| { - by_ref || !(ty.is_unit(ctx) | ty.is_bool(ctx) | ty.is_uint(ctx)) - }) - .unwrap_or(false) - }) { - return true; - } - - false - }; - - let cg = call_graph::build_call_graph(ir, functions); - let functions = call_graph::callee_first_order(&cg); - - for function in functions { - if let Err(ir_error) = match tree_type { - parsed::TreeType::Predicate => { - // Inline everything for predicates - sway_ir::optimize::inline_all_function_calls(ir, &function) - } - _ => sway_ir::optimize::inline_some_function_calls(ir, &function, inline_heuristic), - } { - return err( - Vec::new(), - vec![CompileError::InternalOwned( - ir_error.to_string(), - span::Span::dummy(), - )], - ); - } - } - ok((), Vec::new(), Vec::new()) -} - -fn combine_constants( - handler: &Handler, - ir: &mut Context, - functions: &[Function], -) -> Result<(), ErrorEmitted> { - for function in functions { - if let Err(ir_error) = sway_ir::optimize::combine_constants(ir, function) { - return Err(handler.emit_err(CompileError::InternalOwned( - ir_error.to_string(), - span::Span::dummy(), - ))); - } - } - Ok(()) -} - -fn dce( - handler: &Handler, - ir: &mut Context, - entry_functions: &[Function], -) -> Result<(), ErrorEmitted> { - // Remove entire dead functions first. - for module in ir.module_iter() { - sway_ir::optimize::func_dce(ir, &module, entry_functions); - } - - // Then DCE all the remaining functions. - for module in ir.module_iter() { - for function in module.function_iter(ir) { - if let Err(ir_error) = sway_ir::optimize::dce(ir, &function) { - return Err(handler.emit_err(CompileError::InternalOwned( - ir_error.to_string(), - span::Span::dummy(), - ))); - } - } - } - Ok(()) -} - -fn simplify_cfg( - handler: &Handler, - ir: &mut Context, - functions: &[Function], -) -> Result<(), ErrorEmitted> { - for function in functions { - if let Err(ir_error) = sway_ir::optimize::simplify_cfg(ir, function) { - return Err(handler.emit_err(CompileError::InternalOwned( - ir_error.to_string(), - span::Span::dummy(), - ))); - } - } - Ok(()) -} - /// Given input Sway source code, compile to [CompiledBytecode], containing the asm in bytecode form. pub fn compile_to_bytecode( engines: Engines<'_>, diff --git a/sway-core/src/metadata.rs b/sway-core/src/metadata.rs index 65e46e7fe7f..e94db03fd9d 100644 --- a/sway-core/src/metadata.rs +++ b/sway-core/src/metadata.rs @@ -140,6 +140,12 @@ impl MetadataManager { } /// Gets Inline information from metadata index. + /// TODO: We temporarily allow this because we need this + /// in the sway-ir inliner, but cannot access it. So the code + /// itself has been (modified and) copied there. When we decide + /// on the right place for Metadata to be + /// (and how it can be accessed form sway-ir), this will be fixed. + #[allow(dead_code)] pub(crate) fn md_to_inline( &mut self, context: &Context, diff --git a/sway-ir/Cargo.toml b/sway-ir/Cargo.toml index fe61c703435..1175306a5a2 100644 --- a/sway-ir/Cargo.toml +++ b/sway-ir/Cargo.toml @@ -10,6 +10,7 @@ description = "Sway intermediate representation." [dependencies] anyhow = "1.0" +downcast-rs = "1.2.0" filecheck = "0.5" generational-arena = "0.2" peg = "0.7" diff --git a/sway-ir/src/bin/opt.rs b/sway-ir/src/bin/opt.rs index 1aba855669d..d459f7e7020 100644 --- a/sway-ir/src/bin/opt.rs +++ b/sway-ir/src/bin/opt.rs @@ -4,7 +4,10 @@ use std::{ }; use anyhow::anyhow; -use sway_ir::{ConstCombinePass, DCEPass, InlinePass, Mem2RegPass, PassManager, SimplifyCfgPass}; +use sway_ir::{ + create_const_combine_pass, create_dce_pass, create_inline_pass, create_mem2reg_pass, + create_simplify_cfg_pass, PassManager, PassManagerConfig, +}; // ------------------------------------------------------------------------------------------------- @@ -12,11 +15,11 @@ fn main() -> Result<(), anyhow::Error> { // Maintain a list of named pass functions for delegation. let mut pass_mgr = PassManager::default(); - pass_mgr.register::(); - pass_mgr.register::(); - pass_mgr.register::(); - pass_mgr.register::(); - pass_mgr.register::(); + pass_mgr.register(create_const_combine_pass()); + pass_mgr.register(create_inline_pass()); + pass_mgr.register(create_simplify_cfg_pass()); + pass_mgr.register(create_dce_pass()); + pass_mgr.register(create_mem2reg_pass()); // Build the config from the command line. let config = ConfigBuilder::build(&pass_mgr, std::env::args())?; @@ -28,9 +31,10 @@ fn main() -> Result<(), anyhow::Error> { let mut ir = sway_ir::parser::parse(&input_str)?; // Perform optimisation passes in order. - for pass in config.passes { - pass_mgr.run(pass.name.as_ref(), &mut ir)?; - } + let pm_config = PassManagerConfig { + to_run: config.passes.iter().map(|pass| pass.name.clone()).collect(), + }; + pass_mgr.run(&mut ir, &pm_config)?; // Write the output file or standard out. write_to_output(ir, &config.output_path)?; @@ -164,9 +168,8 @@ impl<'a, I: Iterator> ConfigBuilder<'a, I> { } fn build_pass(mut self, name: &str) -> Result { - if self.pass_mgr.contains(name) { + if self.pass_mgr.is_registered(name) { self.cfg.passes.push(name.into()); - self.next = self.rest.next(); self.build_root() } else { Err(anyhow!( diff --git a/sway-ir/src/optimize/constants.rs b/sway-ir/src/optimize/constants.rs index ff29d777262..0bffc001012 100644 --- a/sway-ir/src/optimize/constants.rs +++ b/sway-ir/src/optimize/constants.rs @@ -11,40 +11,36 @@ use crate::{ function::Function, instruction::Instruction, value::{Value, ValueContent, ValueDatum}, - BranchToWithArgs, NamedPass, Predicate, + AnalysisResults, BranchToWithArgs, Pass, PassMutability, Predicate, ScopedPass, }; -pub struct ConstCombinePass; - -impl NamedPass for ConstCombinePass { - fn name() -> &'static str { - "constcombine" - } - - fn descr() -> &'static str { - "constant folding." - } - - fn run(ir: &mut Context) -> Result { - Self::run_on_all_fns(ir, combine_constants) +pub fn create_const_combine_pass() -> Pass { + Pass { + name: "constcombine", + descr: "constant folding.", + runner: ScopedPass::FunctionPass(PassMutability::Transform(combine_constants)), } } /// Find constant expressions which can be reduced to fewer opterations. -pub fn combine_constants(context: &mut Context, function: &Function) -> Result { +pub fn combine_constants( + context: &mut Context, + _: &AnalysisResults, + function: Function, +) -> Result { let mut modified = false; loop { - if combine_const_insert_values(context, function) { + if combine_const_insert_values(context, &function) { modified = true; continue; } - if combine_cmp(context, function) { + if combine_cmp(context, &function) { modified = true; continue; } - if combine_cbr(context, function)? { + if combine_cbr(context, &function)? { modified = true; continue; } diff --git a/sway-ir/src/optimize/dce.rs b/sway-ir/src/optimize/dce.rs index f839e25c94b..790d788dd94 100644 --- a/sway-ir/src/optimize/dce.rs +++ b/sway-ir/src/optimize/dce.rs @@ -5,23 +5,26 @@ //! 2. At the time of inspecting a definition, if it has no uses, it is removed. //! This pass does not do CFG transformations. That is handled by simplify_cfg. -use crate::{Block, Context, Function, Instruction, IrError, Module, NamedPass, Value, ValueDatum}; +use crate::{ + AnalysisResults, Block, Context, Function, Instruction, IrError, Module, Pass, PassMutability, + ScopedPass, Value, ValueDatum, +}; use std::collections::{HashMap, HashSet}; -pub struct DCEPass; - -impl NamedPass for DCEPass { - fn name() -> &'static str { - "dce" - } - - fn descr() -> &'static str { - "Dead code elimination." +pub fn create_dce_pass() -> Pass { + Pass { + name: "dce", + descr: "Dead code elimination.", + runner: ScopedPass::FunctionPass(PassMutability::Transform(dce)), } +} - fn run(ir: &mut Context) -> Result { - Self::run_on_all_fns(ir, dce) +pub fn create_func_dce_pass() -> Pass { + Pass { + name: "func_dce", + descr: "Dead function elimination.", + runner: ScopedPass::ModulePass(PassMutability::Transform(func_dce)), } } @@ -31,7 +34,11 @@ fn can_eliminate_instruction(context: &Context, val: Value) -> bool { } /// Perform dead code (if any) elimination and return true if function modified. -pub fn dce(context: &mut Context, function: &Function) -> Result { +pub fn dce( + context: &mut Context, + _: &AnalysisResults, + function: Function, +) -> Result { // Number of uses that an instruction has. let mut num_uses: HashMap = HashMap::new(); @@ -92,7 +99,15 @@ pub fn dce(context: &mut Context, function: &Function) -> Result /// /// Functions which are `pub` will not be removed and only functions within the passed [`Module`] /// are considered for removal. -pub fn func_dce(context: &mut Context, module: &Module, entry_fns: &[Function]) -> bool { +pub fn func_dce( + context: &mut Context, + _: &AnalysisResults, + module: Module, +) -> Result { + let entry_fns = module + .function_iter(context) + .filter(|func| func.is_entry(context)) + .collect::>(); // Recursively find all the functions called by an entry function. fn grow_called_function_set( context: &Context, @@ -120,7 +135,7 @@ pub fn func_dce(context: &mut Context, module: &Module, entry_fns: &[Function]) // Gather our entry functions together into a set. let mut called_fns: HashSet = HashSet::new(); for entry_fn in entry_fns { - grow_called_function_set(context, *entry_fn, &mut called_fns); + grow_called_function_set(context, entry_fn, &mut called_fns); } // Gather the functions in the module which aren't called. It's better to collect them @@ -135,5 +150,5 @@ pub fn func_dce(context: &mut Context, module: &Module, entry_fns: &[Function]) module.remove_function(context, &dead_fn); } - modified + Ok(modified) } diff --git a/sway-ir/src/optimize/inline.rs b/sway-ir/src/optimize/inline.rs index d4a856bf1d3..331265166c5 100644 --- a/sway-ir/src/optimize/inline.rs +++ b/sway-ir/src/optimize/inline.rs @@ -9,6 +9,7 @@ use rustc_hash::FxHashMap; use crate::{ asm::AsmArg, block::Block, + call_graph, context::Context, error::IrError, function::Function, @@ -17,29 +18,193 @@ use crate::{ local_var::LocalVar, metadata::{combine, MetadataIndex}, value::{Value, ValueContent, ValueDatum}, - BlockArgument, NamedPass, + AnalysisResults, BlockArgument, Module, Pass, PassMutability, ScopedPass, }; -pub struct InlinePass; +pub fn create_inline_pass() -> Pass { + Pass { + name: "inline", + descr: "inline function calls.", + runner: ScopedPass::ModulePass(PassMutability::Transform(inline_calls)), + } +} -impl NamedPass for InlinePass { - fn name() -> &'static str { - "inline" +pub fn create_inline_in_predicate_pass() -> Pass { + Pass { + name: "inline", + descr: "inline function calls.", + runner: ScopedPass::ModulePass(PassMutability::Transform(inline_in_predicate_module)), } +} - fn descr() -> &'static str { - "inline function calls." +pub fn create_inline_in_non_predicate_pass() -> Pass { + Pass { + name: "inline", + descr: "inline function calls.", + runner: ScopedPass::ModulePass(PassMutability::Transform(inline_in_non_predicate_module)), } +} - fn run(ir: &mut Context) -> Result { - // For now we inline everything into `main()`. Eventually we can be more selective. - let main_fn = ir - .module_iter() - .flat_map(|module| module.function_iter(ir)) - .find(|f| f.get_name(ir) == "main") - .unwrap(); - inline_all_function_calls(ir, &main_fn) +/// This is a copy of sway_core::inline::Inline. +/// TODO: Reuse: Depend on sway_core? Move it to sway_types? +#[derive(Debug)] +enum Inline { + Always, + Never, +} +/// This is a copy of sway_core::asm_generation::compiler_constants. +/// TODO: Once we have a target specific IR generator / legalizer, +/// use that to mark related functions as ALWAYS_INLINE. +/// Then we no longer depend on this const value below. +const NUM_ARG_REGISTERS: u8 = 6; + +fn metadata_to_inline(context: &Context, md_idx: Option) -> Option { + fn for_each_md_idx Option>( + context: &Context, + md_idx: Option, + mut f: F, + ) -> Option { + // If md_idx is not None and is a list then try them all. + md_idx.and_then(|md_idx| { + if let Some(md_idcs) = md_idx.get_content(context).unwrap_list() { + md_idcs.iter().find_map(|md_idx| f(*md_idx)) + } else { + f(md_idx) + } + }) + } + for_each_md_idx(context, md_idx, |md_idx| { + // Create a new inline and save it in the cache. + md_idx + .get_content(context) + .unwrap_struct("inline", 1) + .and_then(|fields| fields[0].unwrap_string()) + .and_then(|inline_str| { + let inline = match inline_str { + "always" => Some(Inline::Always), + "never" => Some(Inline::Never), + _otherwise => None, + }?; + Some(inline) + }) + }) +} + +/// Inline function calls based on two conditions: +/// 1. The program we're compiling is a "predicate". Predicates cannot jump backwards which means +/// that supporting function calls (i.e. without inlining) is not possible. This is a protocol +/// restriction and not a heuristic. +/// 2. If the program is not a "predicate" then, we rely on some heuristic which is described below +/// in the `inline_heuristc` closure in `inline_in_non_predicate_module`. +pub fn inline_in_predicate_module( + context: &mut Context, + _: &AnalysisResults, + module: Module, +) -> Result { + let cg = + call_graph::build_call_graph(context, &module.function_iter(context).collect::>()); + + let functions = call_graph::callee_first_order(&cg); + + let mut modified = false; + + for function in functions { + modified |= inline_all_function_calls(context, &function)?; + } + Ok(modified) +} + +pub fn inline_in_non_predicate_module( + context: &mut Context, + _: &AnalysisResults, + module: Module, +) -> Result { + // Inspect ALL calls and count how often each function is called. + let call_counts: HashMap = + module + .function_iter(context) + .fold(HashMap::new(), |mut counts, func| { + for (_block, ins) in func.instruction_iter(context) { + if let Some(Instruction::Call(callee, _args)) = ins.get_instruction(context) { + counts + .entry(*callee) + .and_modify(|count| *count += 1) + .or_insert(1); + } + } + counts + }); + + let inline_heuristic = |ctx: &Context, func: &Function, _call_site: &Value| { + let attributed_inline = metadata_to_inline(ctx, func.get_metadata(ctx)); + match attributed_inline { + Some(Inline::Always) => { + // TODO: check if inlining of function is possible + // return true; + } + Some(Inline::Never) => { + return false; + } + None => {} + } + + // For now, pending improvements to ASMgen for calls, we must inline any function which has + // too many args. + if func.args_iter(ctx).count() as u8 > NUM_ARG_REGISTERS { + return true; + } + + // If the function is called only once then definitely inline it. + if call_counts.get(func).copied().unwrap_or(0) == 1 { + return true; + } + + // If the function is (still) small then also inline it. + const MAX_INLINE_INSTRS_COUNT: usize = 4; + if func.num_instructions(ctx) <= MAX_INLINE_INSTRS_COUNT { + return true; + } + + // As per https://github.com/FuelLabs/sway/issues/2819 we can hit problems if a function + // argument is used as a pointer (probably because it has a ref type) although it actually + // isn't one. Ref type args which aren't pointers need to be inlined. + if func.args_iter(ctx).any(|(_name, arg_val)| { + arg_val + .get_argument_type_and_byref(ctx) + .map(|(ty, by_ref)| { + by_ref || !(ty.is_unit(ctx) | ty.is_bool(ctx) | ty.is_uint(ctx)) + }) + .unwrap_or(false) + }) { + return true; + } + + false + }; + + let cg = + call_graph::build_call_graph(context, &module.function_iter(context).collect::>()); + let functions = call_graph::callee_first_order(&cg); + let mut modified = false; + + for function in functions { + modified |= inline_some_function_calls(context, &function, inline_heuristic)?; + } + Ok(modified) +} + +pub fn inline_calls( + context: &mut Context, + _: &AnalysisResults, + module: Module, +) -> Result { + // For now we inline everything into `main()`. Eventually we can be more selective. + for function in module.function_iter(context) { + if function.get_name(context) == "main" { + return inline_all_function_calls(context, &function); + } } + Ok(false) } /// Inline all calls made from a specific function, effectively removing all `Call` instructions. diff --git a/sway-ir/src/optimize/mem2reg.rs b/sway-ir/src/optimize/mem2reg.rs index 6f9ad2e289b..4598e48fa0b 100644 --- a/sway-ir/src/optimize/mem2reg.rs +++ b/sway-ir/src/optimize/mem2reg.rs @@ -7,27 +7,20 @@ use rustc_hash::FxHashMap; use std::collections::{HashMap, HashSet}; use sway_utils::mapped_stack::MappedStack; -pub struct Mem2RegPass; - -impl NamedPass for Mem2RegPass { - fn name() -> &'static str { - "mem2reg" - } - - fn descr() -> &'static str { - "Promote local memory to SSA registers." - } +use crate::{ + compute_dom_fronts, dominator::compute_dom_tree, AnalysisResults, Block, BranchToWithArgs, + Context, DomTree, Function, Instruction, IrError, LocalVar, Pass, PassMutability, PostOrder, + ScopedPass, Type, Value, ValueDatum, +}; - fn run(ir: &mut Context) -> Result { - Self::run_on_all_fns(ir, promote_to_registers) +pub fn create_mem2reg_pass() -> Pass { + Pass { + name: "mem2reg", + descr: "Promote local memory to SSA registers.", + runner: ScopedPass::FunctionPass(PassMutability::Transform(promote_to_registers)), } } -use crate::{ - compute_dom_fronts, dominator::compute_dom_tree, Block, BranchToWithArgs, Context, DomTree, - Function, Instruction, IrError, LocalVar, NamedPass, PostOrder, Type, Value, ValueDatum, -}; - // Check if a value is a valid (for our optimization) local pointer fn get_validate_local_var( context: &Context, @@ -140,16 +133,20 @@ pub fn compute_livein( /// We promote only locals of non-copy type, whose every use is in a `get_local` /// without offsets, and the result of such a `get_local` is used only in a load /// or a store. -pub fn promote_to_registers(context: &mut Context, function: &Function) -> Result { - let safe_locals = filter_usable_locals(context, function); +pub fn promote_to_registers( + context: &mut Context, + _: &AnalysisResults, + function: Function, +) -> Result { + let safe_locals = filter_usable_locals(context, &function); if safe_locals.is_empty() { return Ok(false); } - let (dom_tree, po) = compute_dom_tree(context, function); + let (dom_tree, po) = compute_dom_tree(context, &function); let dom_fronts = compute_dom_fronts(context, &dom_tree); - let liveins = compute_livein(context, function, &po, &safe_locals); + let liveins = compute_livein(context, &function, &po, &safe_locals); // A list of the PHIs we insert in this transform. let mut new_phi_tracker = HashSet::<(String, Block)>::new(); @@ -168,7 +165,7 @@ pub fn promote_to_registers(context: &mut Context, function: &Function) -> Resul if let ValueDatum::Instruction(Instruction::Store { dst_val, .. }) = context.values[inst.0].value { - match get_validate_local_var(context, function, &dst_val) { + match get_validate_local_var(context, &function, &dst_val) { Some((local, var)) if safe_locals.contains(&local) => { worklist.push((local, var.get_type(context), block)); } @@ -323,7 +320,7 @@ pub fn promote_to_registers(context: &mut Context, function: &Function) -> Resul let mut delete_insts = Vec::<(Block, Value)>::new(); record_rewrites( context, - function, + &function, &dom_tree, function.get_entry_block(context), &safe_locals, diff --git a/sway-ir/src/optimize/simplify_cfg.rs b/sway-ir/src/optimize/simplify_cfg.rs index eb866f128a9..be003cad894 100644 --- a/sway-ir/src/optimize/simplify_cfg.rs +++ b/sway-ir/src/optimize/simplify_cfg.rs @@ -12,30 +12,26 @@ use rustc_hash::{FxHashMap, FxHashSet}; use crate::{ block::Block, context::Context, error::IrError, function::Function, instruction::Instruction, - value::ValueDatum, BranchToWithArgs, NamedPass, Value, + value::ValueDatum, AnalysisResults, BranchToWithArgs, Pass, PassMutability, ScopedPass, Value, }; -pub struct SimplifyCfgPass; - -impl NamedPass for SimplifyCfgPass { - fn name() -> &'static str { - "simplifycfg" - } - - fn descr() -> &'static str { - "merge or remove redundant blocks." - } - - fn run(ir: &mut Context) -> Result { - Self::run_on_all_fns(ir, simplify_cfg) +pub fn create_simplify_cfg_pass() -> Pass { + Pass { + name: "simplifycfg", + descr: "merge or remove redundant blocks.", + runner: ScopedPass::FunctionPass(PassMutability::Transform(simplify_cfg)), } } -pub fn simplify_cfg(context: &mut Context, function: &Function) -> Result { +pub fn simplify_cfg( + context: &mut Context, + _: &AnalysisResults, + function: Function, +) -> Result { let mut modified = false; - modified |= remove_dead_blocks(context, function)?; - modified |= merge_blocks(context, function)?; - modified |= unlink_empty_blocks(context, function)?; + modified |= remove_dead_blocks(context, &function)?; + modified |= merge_blocks(context, &function)?; + modified |= unlink_empty_blocks(context, &function)?; Ok(modified) } diff --git a/sway-ir/src/pass_manager.rs b/sway-ir/src/pass_manager.rs index 344838d080f..74b2ccd61ca 100644 --- a/sway-ir/src/pass_manager.rs +++ b/sway-ir/src/pass_manager.rs @@ -1,46 +1,159 @@ -use crate::{Context, Function, IrError}; -use std::collections::HashMap; - -pub trait NamedPass { - fn name() -> &'static str; - fn descr() -> &'static str; - fn run(ir: &mut Context) -> Result; - - fn run_on_all_fns Result>( - ir: &mut Context, - mut run_on_fn: F, - ) -> Result { - let funcs = ir - .module_iter() - .flat_map(|module| module.function_iter(ir)) - .collect::>(); - let mut modified = false; - for func in funcs { - if run_on_fn(ir, &func)? { - modified = true; - } +use crate::{Context, Function, IrError, Module}; +use downcast_rs::{impl_downcast, Downcast}; +use std::{ + any::{type_name, TypeId}, + collections::{hash_map, HashMap}, +}; + +/// Result of an analysis. Specific result must be downcasted to. +pub trait AnalysisResultT: Downcast {} +impl_downcast!(AnalysisResultT); +pub type AnalysisResult = Box; + +/// Program scope over which a pass executes. +pub trait PassScope { + fn get_arena_idx(&self) -> generational_arena::Index; +} +impl PassScope for Module { + fn get_arena_idx(&self) -> generational_arena::Index { + self.0 + } +} +impl PassScope for Function { + fn get_arena_idx(&self) -> generational_arena::Index { + self.0 + } +} + +/// Is a pass an Analysis or a Transformation over the IR? +pub enum PassMutability { + /// An analysis pass, producing an analysis result. + Analysis(fn(&mut Context, analyses: &AnalysisResults, S) -> Result), + /// A pass over the IR that can possibly modify it. + Transform(fn(&mut Context, analyses: &AnalysisResults, S) -> Result), +} + +/// A concrete version of [PassScope]. +pub enum ScopedPass { + ModulePass(PassMutability), + FunctionPass(PassMutability), +} + +pub struct Pass { + pub name: &'static str, + pub descr: &'static str, + pub runner: ScopedPass, +} + +impl Pass { + pub fn is_analysis(&self) -> bool { + match &self.runner { + ScopedPass::ModulePass(pm) => matches!(pm, PassMutability::Analysis(_)), + ScopedPass::FunctionPass(pm) => matches!(pm, PassMutability::Analysis(_)), } - Ok(modified) + } + pub fn is_transform(&self) -> bool { + !self.is_analysis() } } -pub type NamePassPair = (&'static str, fn(&mut Context) -> Result); +#[derive(Default)] +pub struct AnalysisResults { + // Hash from (AnalysisResultT, (PassScope, Scope Identity)) to an actual result. + results: HashMap<(TypeId, (TypeId, generational_arena::Index)), AnalysisResult>, +} + +impl AnalysisResults { + /// Get the results of an analysis. + /// Example analyses.get_analysis_result::(foo). + pub fn get_analysis_result(&self, scope: S) -> &T { + self.results + .get(&( + TypeId::of::(), + (TypeId::of::(), scope.get_arena_idx()), + )) + .unwrap_or_else(|| { + panic!( + "Internal error. Analysis result {} unavailable for {} with idx {:?}", + type_name::(), + type_name::(), + scope.get_arena_idx() + ) + }) + .downcast_ref() + .unwrap() + } + + /// Add a new result. + pub fn add_result(&mut self, scope: S, result: AnalysisResult) { + self.results.insert( + ( + (*result).type_id(), + (TypeId::of::(), scope.get_arena_idx()), + ), + result, + ); + } +} #[derive(Default)] pub struct PassManager { - passes: HashMap<&'static str, NamePassPair>, + passes: HashMap<&'static str, Pass>, + analyses: AnalysisResults, } impl PassManager { - pub fn register(&mut self) { - self.passes.insert(T::name(), (T::descr(), T::run)); + /// Register a pass. Should be called only once for each pass. + pub fn register(&mut self, pass: Pass) -> &'static str { + let pass_name = pass.name; + match self.passes.entry(pass.name) { + hash_map::Entry::Occupied(_) => { + panic!("Trying to register an already registered pass"); + } + hash_map::Entry::Vacant(entry) => { + entry.insert(pass); + } + } + pass_name } - pub fn run(&self, name: &str, ir: &mut Context) -> Result { - self.passes.get(name).expect("Unknown pass name!").1(ir) + /// Run the passes specified in `config`. + pub fn run(&mut self, ir: &mut Context, config: &PassManagerConfig) -> Result { + let mut modified = false; + for pass in &config.to_run { + let pass_t = self.passes.get(pass.as_str()).expect("Unregistered pass"); + for m in ir.module_iter() { + match &pass_t.runner { + ScopedPass::ModulePass(mp) => match mp { + PassMutability::Analysis(analysis) => { + let result = analysis(ir, &self.analyses, m)?; + self.analyses.add_result(m, result); + } + PassMutability::Transform(transform) => { + modified |= transform(ir, &self.analyses, m)?; + } + }, + ScopedPass::FunctionPass(fp) => { + for f in m.function_iter(ir) { + match fp { + PassMutability::Analysis(analysis) => { + let result = analysis(ir, &self.analyses, f)?; + self.analyses.add_result(f, result); + } + PassMutability::Transform(transform) => { + modified |= transform(ir, &self.analyses, f)?; + } + } + } + } + } + } + } + Ok(modified) } - pub fn contains(&self, name: &str) -> bool { + /// Is `name` a registered pass? + pub fn is_registered(&self, name: &str) -> bool { self.passes.contains_key(name) } @@ -48,10 +161,15 @@ impl PassManager { let summary = self .passes .iter() - .map(|(name, (descr, _))| format!(" {name:16} - {descr}")) + .map(|(name, pass)| format!(" {name:16} - {}", pass.descr)) .collect::>() .join("\n"); format!("Valid pass names are:\n\n{summary}",) } } + +/// Configuration for the pass manager to run passes. +pub struct PassManagerConfig { + pub to_run: Vec, +} diff --git a/sway-ir/tests/tests.rs b/sway-ir/tests/tests.rs index df67d732d35..84da721caab 100644 --- a/sway-ir/tests/tests.rs +++ b/sway-ir/tests/tests.rs @@ -1,6 +1,9 @@ use std::path::PathBuf; -use sway_ir::{optimize as opt, Context}; +use sway_ir::{ + create_const_combine_pass, create_dce_pass, create_mem2reg_pass, create_simplify_cfg_pass, + optimize as opt, Context, PassManager, PassManagerConfig, +}; // ------------------------------------------------------------------------------------------------- // Utility for finding test files and running FileCheck. See actual pass invocations below. @@ -117,13 +120,11 @@ fn inline() { #[test] fn constants() { run_tests("constants", |_first_line, ir: &mut Context| { - let funcs: Vec<_> = ir - .module_iter() - .flat_map(|module| module.function_iter(ir)) - .collect(); - funcs.into_iter().fold(false, |acc, func| { - sway_ir::optimize::combine_constants(ir, &func).unwrap() || acc - }) + let mut pass_mgr = PassManager::default(); + let mut pmgr_config = PassManagerConfig { to_run: vec![] }; + let pass = pass_mgr.register(create_const_combine_pass()); + pmgr_config.to_run.push(pass.to_string()); + pass_mgr.run(ir, &pmgr_config).unwrap() }) } @@ -133,13 +134,11 @@ fn constants() { #[test] fn simplify_cfg() { run_tests("simplify_cfg", |_first_line, ir: &mut Context| { - let funcs: Vec<_> = ir - .module_iter() - .flat_map(|module| module.function_iter(ir)) - .collect(); - funcs.into_iter().fold(false, |acc, func| { - sway_ir::optimize::simplify_cfg(ir, &func).unwrap() || acc - }) + let mut pass_mgr = PassManager::default(); + let mut pmgr_config = PassManagerConfig { to_run: vec![] }; + let pass = pass_mgr.register(create_simplify_cfg_pass()); + pmgr_config.to_run.push(pass.to_string()); + pass_mgr.run(ir, &pmgr_config).unwrap() }) } @@ -149,13 +148,11 @@ fn simplify_cfg() { #[test] fn dce() { run_tests("dce", |_first_line, ir: &mut Context| { - let funcs: Vec<_> = ir - .module_iter() - .flat_map(|module| module.function_iter(ir)) - .collect(); - funcs.into_iter().fold(false, |acc, func| { - sway_ir::optimize::dce(ir, &func).unwrap() || acc - }) + let mut pass_mgr = PassManager::default(); + let mut pmgr_config = PassManagerConfig { to_run: vec![] }; + let pass = pass_mgr.register(create_dce_pass()); + pmgr_config.to_run.push(pass.to_string()); + pass_mgr.run(ir, &pmgr_config).unwrap() }) } @@ -165,13 +162,11 @@ fn dce() { #[test] fn mem2reg() { run_tests("mem2reg", |_first_line, ir: &mut Context| { - let funcs: Vec<_> = ir - .module_iter() - .flat_map(|module| module.function_iter(ir)) - .collect(); - funcs.into_iter().fold(false, |acc, func| { - sway_ir::optimize::promote_to_registers(ir, &func).unwrap() || acc - }) + let mut pass_mgr = PassManager::default(); + let mut pmgr_config = PassManagerConfig { to_run: vec![] }; + let pass = pass_mgr.register(create_mem2reg_pass()); + pmgr_config.to_run.push(pass.to_string()); + pass_mgr.run(ir, &pmgr_config).unwrap() }) } diff --git a/test/src/ir_generation/mod.rs b/test/src/ir_generation/mod.rs index 38cd577ad7b..8d93317efc9 100644 --- a/test/src/ir_generation/mod.rs +++ b/test/src/ir_generation/mod.rs @@ -7,8 +7,12 @@ use std::{ use anyhow::Result; use colored::Colorize; use sway_core::{ - compile_ir_to_asm, compile_to_ast, decl_engine::DeclEngine, inline_function_calls, - ir_generation::compile_program, namespace, BuildTarget, Engines, TypeEngine, + compile_ir_to_asm, compile_to_ast, decl_engine::DeclEngine, ir_generation::compile_program, + language::parsed::TreeType, namespace, BuildTarget, Engines, TypeEngine, +}; +use sway_ir::{ + create_inline_in_non_predicate_pass, create_inline_in_predicate_pass, PassManager, + PassManagerConfig, }; pub(super) async fn run(filter_regex: Option<®ex::Regex>) -> Result<()> { @@ -180,15 +184,17 @@ pub(super) async fn run(filter_regex: Option<®ex::Regex>) -> Result<()> { _ => (), }; - // Now we're working with all functions in the module. - let all_functions = ir - .module_iter() - .flat_map(|module| module.function_iter(&ir)) - .collect::>(); - if optimisation_inline { - let inline_res = inline_function_calls(&mut ir, &all_functions, &tree_type); - if !inline_res.errors.is_empty() { + let mut pass_mgr = PassManager::default(); + let mut pmgr_config = PassManagerConfig { to_run: vec![] }; + let inline = if matches!(tree_type, TreeType::Predicate) { + pass_mgr.register(create_inline_in_predicate_pass()) + } else { + pass_mgr.register(create_inline_in_non_predicate_pass()) + }; + pmgr_config.to_run.push(inline.to_string()); + let inline_res = pass_mgr.run(&mut ir, &pmgr_config); + if inline_res.is_err() { panic!( "Failed to compile test {}:\n{}", path.display(),