Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 41 additions & 27 deletions src/attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@ use gccjit::Function;
#[cfg(feature = "master")]
use rustc_abi::{CanonAbi, InterruptKind};
#[cfg(feature = "master")]
use rustc_data_structures::fx::FxHashSet;
#[cfg(feature = "master")]
use rustc_hir::attrs::InlineAttr;
use rustc_hir::attrs::InstructionSetAttr;
#[cfg(feature = "master")]
use rustc_middle::middle::codegen_fn_attrs::CodegenFnAttrFlags;
#[cfg(feature = "master")]
use rustc_middle::mir::TerminatorKind;
use rustc_middle::ty;
#[cfg(feature = "master")]
use rustc_span::def_id::DefId;
use rustc_target::callconv::FnAbi;
#[cfg(feature = "master")]
use rustc_target::spec::Arch;
Expand All @@ -20,32 +24,48 @@ use crate::base;
use crate::context::CodegenCx;
use crate::gcc_util::to_gcc_features;

/// Checks if the function `instance` is recursively inline.
/// Returns `false` if a functions is guaranteed to be non-recursive, and `true` if it *might* be recursive.
/// Check forced-inline call chains for cycles. Merely calling another
/// always-inline function is not recursion.

@antoyo antoyo Sep 15, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Explain in the doc comment what is the return value.

View changes since the review

#[cfg(feature = "master")]
fn recursively_inline<'gcc, 'tcx>(
cx: &CodegenCx<'gcc, 'tcx>,
instance: ty::Instance<'tcx>,
) -> bool {
// No body, so we can't check if this is recursively inline, so we assume it is.
if !cx.tcx.is_mir_available(instance.def_id()) {
return true;
}
// `expect_local` ought to never fail: we should be checking a function within this codegen unit.
let body = cx.tcx.optimized_mir(instance.def_id());
for block in body.basic_blocks.iter() {
let Some(ref terminator) = block.terminator else { continue };
// I assume that the recursive-inline issue applies only to functions, and not to drops.
// In principle, a recursive, `#[inline(always)]` drop could(?) exist, but I don't think it does.
Comment on lines -38 to -39

@antoyo antoyo Sep 15, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please keep this comment as it is useful.

View changes since the review

let TerminatorKind::Call { ref func, .. } = terminator.kind else { continue };
let Some((def, _args)) = func.const_fn_def() else { continue };
// Check if the called function is recursively inline.
if matches!(
cx.tcx.codegen_fn_attrs(def).inline,
InlineAttr::Always | InlineAttr::Force { .. }
) {
// Keep the DFS on the heap: valid forced-inline chains can be arbitrarily
// deep, independently of the compiler thread's remaining call stack.
let mut pending: Vec<(DefId, bool)> = vec![(instance.def_id(), false)];

@antoyo antoyo Sep 15, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a comment to explain what the bool represents in this Vec.

View changes since the review

let mut active = FxHashSet::default();
while let Some((def, finishing)) = pending.pop() {
if finishing {
active.remove(&def);
cx.inline_recursion.borrow_mut().insert(def, false);
continue;
}
let cached = cx.inline_recursion.borrow().get(&def).copied();
if cached == Some(false) {
continue;
}
if cached == Some(true) || active.contains(&def) || !cx.tcx.is_mir_available(def) {
let mut cache = cx.inline_recursion.borrow_mut();
cache.insert(def, true);
for caller in active {
cache.insert(caller, true);
}
return true;
}
active.insert(def);
pending.push((def, true));
for block in cx.tcx.optimized_mir(def).basic_blocks.iter().rev() {

@antoyo antoyo Sep 15, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this loop in reverse order because calls are terminators, so at the end of basic blocks?
Add a comment to explain why this is in reverse order.

View changes since the review

let Some(ref terminator) = block.terminator else { continue };
let TerminatorKind::Call { ref func, .. } = terminator.kind else { continue };
let Some((callee, _)) = func.const_fn_def() else { continue };
if matches!(
cx.tcx.codegen_fn_attrs(callee).inline,
InlineAttr::Always | InlineAttr::Force { .. }
) {
pending.push((callee, false));
}
}
}
false
}
Expand All @@ -60,14 +80,8 @@ fn inline_attr<'gcc, 'tcx>(
) -> Option<FnAttribute<'gcc>> {
match inline {
InlineAttr::Always => {
// We can't simply always return `always_inline` unconditionally.
// It is *NOT A HINT* and does not work for recursive functions.
//
// So, it can only be applied *if*:
// The current function does not call any functions marked `#[inline(always)]`.
//
// That prevents issues steming from recursive `#[inline(always)]` at a *relatively* small cost.
// We *only* need to check all the terminators of a function marked with this attribute.
// GCC cannot force recursive call chains inline. Preserve the
// guarantee for acyclic chains, including nested intrinsic wrappers.
if recursively_inline(cx, instance) {
Some(FnAttribute::Inline)
} else {
Expand Down
6 changes: 6 additions & 0 deletions src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ use rustc_middle::ty::{self, ExistentialTraitRef, Instance, Ty, TyCtxt};
#[cfg(feature = "master")]
use rustc_session::config::DebugInfo;
use rustc_session::{PointerAuthSchema, Session};
#[cfg(feature = "master")]
use rustc_span::def_id::DefId;
use rustc_span::{DUMMY_SP, Span, Symbol};
use rustc_target::spec::{HasTargetSpec, HasX86AbiOpt, Target, TlsModel, X86Abi};

Expand Down Expand Up @@ -49,6 +51,8 @@ pub struct CodegenCx<'gcc, 'tcx> {

pub functions: RefCell<FxHashMap<String, Function<'gcc>>>,
pub intrinsics: RefCell<FxHashMap<String, Function<'gcc>>>,
#[cfg(feature = "master")]
pub inline_recursion: RefCell<FxHashMap<DefId, bool>>,

@antoyo antoyo Sep 15, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a comment to explain why we need this field and what it is used for and what does the bool represent.

View changes since the review


pub tls_model: gccjit::TlsModel,

Expand Down Expand Up @@ -263,6 +267,8 @@ impl<'gcc, 'tcx> CodegenCx<'gcc, 'tcx> {
function_address_names: Default::default(),
functions: RefCell::new(functions),
intrinsics: RefCell::new(FxHashMap::default()),
#[cfg(feature = "master")]
inline_recursion: Default::default(),

tls_model,

Expand Down
31 changes: 31 additions & 0 deletions tests/asm/always_inline.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//@ assembly-output: emit-asm
//@ only-x86_64
//@ compile-flags: -Copt-level=0

#![crate_type = "lib"]
#![no_std]

// At -O0, an ordinary inline hint is insufficient: the entire chain must retain
// always_inline even though its wrappers call other always-inline functions.
#[inline(always)]
fn leaf(x: u32) -> u32 {
x ^ 0xa5a5
}

#[inline(always)]
fn mid(x: u32) -> u32 {
leaf(x).wrapping_mul(3)
}

#[inline(always)]
fn top(x: u32) -> u32 {
mid(x).wrapping_add(7)
}

// CHECK-LABEL: {{^"?_?}}entry{{"?}}:
// CHECK-NOT: call
// CHECK: ret
#[no_mangle]
pub fn entry(x: u32) -> u32 {
top(x)
}
Loading