Skip to main content

cairo_lang_lowering/inline/
mod.rs

1#[cfg(test)]
2mod test;
3
4pub mod statements_weights;
5
6use cairo_lang_defs::diagnostic_utils::StableLocation;
7use cairo_lang_defs::ids::LanguageElementId;
8use cairo_lang_diagnostics::{Diagnostics, Maybe};
9use cairo_lang_semantic::items::function_with_body::FunctionWithBodySemantic;
10use cairo_lang_semantic::items::functions::InlineConfiguration;
11use cairo_lang_utils::casts::IntoOrPanic;
12use cairo_lang_utils::ordered_hash_map::OrderedHashMap;
13use cairo_lang_utils::unordered_hash_map::UnorderedHashMap;
14use itertools::{Itertools, zip_eq};
15use salsa::Database;
16
17use crate::blocks::Blocks;
18use crate::db::LoweringGroup;
19use crate::diagnostic::{
20    LoweringDiagnostic, LoweringDiagnosticKind, LoweringDiagnostics, LoweringDiagnosticsBuilder,
21};
22use crate::ids::{
23    ConcreteFunctionWithBodyId, ConcreteFunctionWithBodyLongId, FunctionWithBodyId,
24    FunctionWithBodyLongId, LocationId,
25};
26use crate::optimizations::const_folding::ConstFoldingContext;
27use crate::utils::{InliningStrategy, Rebuilder, RebuilderEx};
28use crate::{
29    Block, BlockEnd, BlockId, DependencyType, Lowered, LoweringStage, Statement, StatementCall,
30    VarRemapping, Variable, VariableArena, VariableId,
31};
32
33pub fn get_inline_diagnostics<'db>(
34    db: &'db dyn Database,
35    function_id: FunctionWithBodyId<'db>,
36) -> Maybe<Diagnostics<'db, LoweringDiagnostic<'db>>> {
37    let inline_config = match function_id.long(db) {
38        FunctionWithBodyLongId::Semantic(id) => db.function_declaration_inline_config(*id)?,
39        FunctionWithBodyLongId::Generated { .. } => InlineConfiguration::None,
40    };
41    let mut diagnostics = LoweringDiagnostics::default();
42
43    if let InlineConfiguration::Always(_) = inline_config
44        && db.in_cycle(function_id, crate::DependencyType::Call)?
45    {
46        diagnostics.report(
47            function_id.base_semantic_function(db).untyped_stable_ptr(db),
48            LoweringDiagnosticKind::CannotInlineFunctionThatMightCallItself,
49        );
50    }
51
52    Ok(diagnostics.build())
53}
54
55/// Query implementation of [LoweringGroup::priv_should_inline].
56#[salsa::tracked]
57pub fn priv_should_inline<'db>(
58    db: &'db dyn Database,
59    function_id: ConcreteFunctionWithBodyId<'db>,
60) -> Maybe<bool> {
61    if db.priv_never_inline(function_id)? {
62        return Ok(false);
63    }
64    // Prevents inlining of functions that may call themselves, by checking if the base of the
65    // function (the function without specialization) is in a call cycle (we cannot use the
66    // specialized version as it may call the base function with different specialization which may
67    // cause long inlining chains).
68    // TODO(Tomerstarkware): allow inlining of specialized recursive functions for one level of
69    // recursion.
70    let base = match function_id.long(db) {
71        ConcreteFunctionWithBodyLongId::Semantic(_)
72        | ConcreteFunctionWithBodyLongId::Generated(_) => function_id,
73        ConcreteFunctionWithBodyLongId::Specialized(specialized) => specialized.long(db).base,
74    };
75    if db.concrete_in_cycle(base, DependencyType::Call, LoweringStage::PreOptimizations)? {
76        return Ok(false);
77    }
78
79    match (db.optimizations().inlining_strategy(), function_inline_config(db, function_id)?) {
80        (_, InlineConfiguration::Always(_)) => Ok(true),
81        (InliningStrategy::Avoid, _) | (_, InlineConfiguration::Never(_)) => Ok(false),
82        (_, InlineConfiguration::Should(_)) => Ok(true),
83        (InliningStrategy::Default, InlineConfiguration::None) => {
84            /// The default threshold for inlining small functions. Decided according to sample
85            /// contracts profiling.
86            const DEFAULT_INLINE_SMALL_FUNCTIONS_THRESHOLD: usize = 120;
87            should_inline_lowered(db, function_id, DEFAULT_INLINE_SMALL_FUNCTIONS_THRESHOLD)
88        }
89        (InliningStrategy::InlineSmallFunctions(threshold), InlineConfiguration::None) => {
90            should_inline_lowered(db, function_id, threshold)
91        }
92    }
93}
94
95/// Query implementation of [LoweringGroup::priv_never_inline].
96#[salsa::tracked]
97pub fn priv_never_inline<'db>(
98    db: &'db dyn Database,
99    function_id: ConcreteFunctionWithBodyId<'db>,
100) -> Maybe<bool> {
101    Ok(matches!(function_inline_config(db, function_id)?, InlineConfiguration::Never(_)))
102}
103
104/// Returns the [InlineConfiguration] of a function.
105fn function_inline_config<'db>(
106    db: &'db dyn Database,
107    function_id: ConcreteFunctionWithBodyId<'db>,
108) -> Maybe<InlineConfiguration<'db>> {
109    match function_id.long(db) {
110        ConcreteFunctionWithBodyLongId::Semantic(id) => {
111            db.function_declaration_inline_config(id.function_with_body_id(db))
112        }
113        ConcreteFunctionWithBodyLongId::Generated(_) => Ok(InlineConfiguration::None),
114        ConcreteFunctionWithBodyLongId::Specialized(specialized) => {
115            function_inline_config(db, specialized.long(db).base)
116        }
117    }
118}
119
120// A heuristic to decide if a function without an inline attribute should be inlined.
121fn should_inline_lowered(
122    db: &dyn Database,
123    function_id: ConcreteFunctionWithBodyId<'_>,
124    inline_small_functions_threshold: usize,
125) -> Maybe<bool> {
126    let weight_of_blocks = db.estimate_size(function_id)?;
127    Ok(weight_of_blocks < inline_small_functions_threshold.into_or_panic())
128}
129/// Context for mapping ids from `lowered` to a new `Lowered` object.
130pub struct Mapper<'db, 'mt, 'l> {
131    db: &'db dyn Database,
132    variables: &'mt mut VariableArena<'db>,
133    lowered: &'l Lowered<'db>,
134    renamed_vars: UnorderedHashMap<VariableId, VariableId>,
135
136    outputs: Vec<VariableId>,
137    inlining_location: StableLocation<'db>,
138
139    /// An offset that is added to all the block IDs in order to translate them into the new
140    /// lowering representation.
141    block_id_offset: BlockId,
142
143    /// Return statements are replaced with goto to this block with the appropriate remapping.
144    return_block_id: BlockId,
145}
146
147impl<'db, 'mt, 'l> Mapper<'db, 'mt, 'l> {
148    pub fn new(
149        db: &'db dyn Database,
150        variables: &'mt mut VariableArena<'db>,
151        lowered: &'l Lowered<'db>,
152        call_stmt: StatementCall<'db>,
153        block_id_offset: usize,
154    ) -> Self {
155        // The input variables need to be renamed to match the inputs to the function call.
156        let renamed_vars = UnorderedHashMap::<VariableId, VariableId>::from_iter(zip_eq(
157            lowered.parameters.iter().cloned(),
158            call_stmt.inputs.iter().map(|var_usage| var_usage.var_id),
159        ));
160
161        let inlining_location = call_stmt.location.long(db).stable_location;
162
163        Self {
164            db,
165            variables,
166            lowered,
167            renamed_vars,
168            block_id_offset: BlockId(block_id_offset),
169            return_block_id: BlockId(block_id_offset + lowered.blocks.len()),
170            outputs: call_stmt.outputs,
171            inlining_location,
172        }
173    }
174}
175
176impl<'db, 'mt> Rebuilder<'db> for Mapper<'db, 'mt, '_> {
177    /// Maps a var id from the original lowering representation to the equivalent id in the
178    /// new lowering representation.
179    /// If the variable wasn't assigned an id yet, a new id is assigned.
180    fn map_var_id(&mut self, orig_var_id: VariableId) -> VariableId {
181        *self.renamed_vars.entry(orig_var_id).or_insert_with(|| {
182            let orig_var = &self.lowered.variables[orig_var_id];
183            self.variables.alloc(Variable {
184                location: orig_var.location.inlined(self.db, self.inlining_location),
185                ..orig_var.clone()
186            })
187        })
188    }
189
190    /// Maps a block id from the original lowering representation to the equivalent id in the
191    /// new lowering representation.
192    fn map_block_id(&mut self, orig_block_id: BlockId) -> BlockId {
193        BlockId(self.block_id_offset.0 + orig_block_id.0)
194    }
195
196    /// Adds the inlining location to a location.
197    fn map_location(&mut self, location: LocationId<'db>) -> LocationId<'db> {
198        location.inlined(self.db, self.inlining_location)
199    }
200
201    fn transform_end(&mut self, end: &mut BlockEnd<'db>) {
202        match end {
203            BlockEnd::Return(returns, _location) => {
204                let remapping = VarRemapping {
205                    remapping: OrderedHashMap::from_iter(zip_eq(
206                        self.outputs.iter().cloned(),
207                        returns.iter().cloned(),
208                    )),
209                };
210                *end = BlockEnd::Goto(self.return_block_id, remapping);
211            }
212            BlockEnd::Panic(_) | BlockEnd::Goto(_, _) | BlockEnd::Match { .. } => {}
213            BlockEnd::NotSet => unreachable!(),
214        }
215    }
216}
217
218/// Inner function for applying inlining.
219///
220/// This function should be called through `apply_inlining` to remove all the lowered blocks in the
221/// error case.
222fn inner_apply_inlining<'db>(
223    db: &'db dyn Database,
224    Lowered { blocks, variables, .. }: &mut Lowered<'db>,
225    calling_function_id: ConcreteFunctionWithBodyId<'db>,
226    mut enable_const_folding: bool,
227) -> Maybe<()> {
228    blocks.has_root()?;
229
230    let mut stack: Vec<std::vec::IntoIter<BlockId>> =
231        vec![(0..blocks.len()).map(BlockId).collect_vec().into_iter()];
232
233    let mut const_folding_ctx = ConstFoldingContext::new(db, calling_function_id, variables);
234
235    enable_const_folding = enable_const_folding && !const_folding_ctx.should_skip_const_folding(db);
236
237    while let Some(mut func_blocks) = stack.pop() {
238        for block_id in func_blocks.by_ref() {
239            let blocks = &mut *blocks;
240            if enable_const_folding
241                && !const_folding_ctx.visit_block_start(block_id, |block_id| &blocks[block_id])
242            {
243                continue;
244            }
245
246            // Read the next block id before `blocks` is borrowed.
247            let next_block_id = blocks.len();
248            let block = &mut blocks[block_id];
249
250            let mut opt_inline_info = None;
251            for (idx, statement) in block.statements.iter_mut().enumerate() {
252                if enable_const_folding {
253                    const_folding_ctx.visit_statement(statement);
254                }
255                if let Some((call_stmt, called_func)) =
256                    should_inline(db, calling_function_id, statement)?
257                {
258                    opt_inline_info = Some((idx, call_stmt.clone(), called_func));
259                    break;
260                }
261            }
262
263            let Some((call_stmt_idx, call_stmt, called_func)) = opt_inline_info else {
264                if enable_const_folding {
265                    const_folding_ctx.visit_block_end(block_id, block);
266                }
267                // Nothing to inline in this block, go to the next block.
268                continue;
269            };
270
271            let inlined_lowered = db.lowered_body(called_func, LoweringStage::PostBaseline)?;
272            inlined_lowered.blocks.has_root()?;
273
274            // Drain the statements starting at the call to the inlined function.
275            let remaining_statements =
276                block.statements.drain(call_stmt_idx..).skip(1).collect_vec();
277
278            // Replace the end of the block with a goto to the root block of the inlined function.
279            let orig_block_end = std::mem::replace(
280                &mut block.end,
281                BlockEnd::Goto(BlockId(next_block_id), VarRemapping::default()),
282            );
283
284            if enable_const_folding {
285                const_folding_ctx.visit_block_end(block_id, block);
286            }
287
288            let mut inline_mapper = Mapper::new(
289                db,
290                const_folding_ctx.variables,
291                inlined_lowered,
292                call_stmt,
293                next_block_id,
294            );
295
296            // Apply the mapper to the inlined blocks and add them as a contiguous chunk to the
297            // blocks.
298            let mut inlined_blocks_ids = inlined_lowered
299                .blocks
300                .iter()
301                .map(|(_block_id, block)| blocks.push(inline_mapper.rebuild_block(block)))
302                .collect_vec();
303
304            // Move the remaining statements and the original block end to a new return block.
305            let return_block_id =
306                blocks.push(Block { statements: remaining_statements, end: orig_block_end });
307            assert_eq!(return_block_id, inline_mapper.return_block_id);
308
309            // Append the id of the return block to the list of blocks in the inlined function.
310            // It is not part of that function, but we want to visit it right after the inlined
311            // function blocks.
312            inlined_blocks_ids.push(return_block_id);
313
314            // Return the remaining blocks from the current function to the stack and add the blocks
315            // of the inlined function to the top of the stack.
316            stack.push(func_blocks);
317            stack.push(inlined_blocks_ids.into_iter());
318            break;
319        }
320    }
321
322    Ok(())
323}
324
325/// Inspects a statement and, when it is an inlinable call, returns the call statement and
326/// the callee function id.
327fn should_inline<'db, 'r>(
328    db: &'db dyn Database,
329    calling_function_id: ConcreteFunctionWithBodyId<'db>,
330    statement: &'r Statement<'db>,
331) -> Maybe<Option<(&'r StatementCall<'db>, ConcreteFunctionWithBodyId<'db>)>>
332where
333    'db: 'r,
334{
335    if let Statement::Call(stmt) = statement {
336        if stmt.with_coupon {
337            return Ok(None);
338        }
339
340        if let Some(called_func) = stmt.function.body(db)? {
341            if let ConcreteFunctionWithBodyLongId::Specialized(specialized) =
342                calling_function_id.long(db)
343                && specialized.long(db).base == called_func
344                && stmt.is_specialization_base_call
345            {
346                // A specialized function should always inline its base.
347                return Ok(Some((stmt, called_func)));
348            }
349
350            if called_func != calling_function_id && db.priv_should_inline(called_func)? {
351                return Ok(Some((stmt, called_func)));
352            }
353        }
354    }
355
356    Ok(None)
357}
358
359/// Applies inlining to a lowered function.
360///
361/// Note that if const folding is enabled, the blocks must be topologically sorted.
362pub fn apply_inlining<'db>(
363    db: &'db dyn Database,
364    function_id: ConcreteFunctionWithBodyId<'db>,
365    lowered: &mut Lowered<'db>,
366    enable_const_folding: bool,
367) -> Maybe<()> {
368    if let Err(diag_added) = inner_apply_inlining(db, lowered, function_id, enable_const_folding) {
369        lowered.blocks = Blocks::new_errored(diag_added);
370    }
371    Ok(())
372}