Do not deduce parameter attributes during CTFE

`fn_abi_of_instance` can look at function bodies to deduce codegen
optimization attributes (namely `ArgAttribute::ReadOnly` and
`ArgAttribute::CapturesNone`) of indirectly-passed parameters.  This can
lead to cycles when performed during typeck, when such attributes are
irrelevant.

This patch breaks a subquery out from `fn_abi_of_instance`,
`fn_abi_of_instance_no_deduced_attrs`, which returns the ABI before such
parameter attributes are deduced; and that new subquery is used in CTFE
instead.
This commit is contained in:
Alan Egerton
2026-01-29 22:05:07 +00:00
parent 765fd2d8c7
commit 55509a9fe4
7 changed files with 231 additions and 81 deletions
@@ -361,7 +361,7 @@ pub fn init_stack_frame(
} else {
ty::List::empty()
};
let callee_fn_abi = self.fn_abi_of_instance(instance, extra_tys)?;
let callee_fn_abi = self.fn_abi_of_instance_no_deduced_attrs(instance, extra_tys)?;
if caller_fn_abi.conv != callee_fn_abi.conv {
throw_ub_custom!(
@@ -899,7 +899,7 @@ pub(super) fn init_drop_in_place_call(
enter_trace_span!(M, resolve::resolve_drop_in_place, ty = ?place.layout.ty);
ty::Instance::resolve_drop_in_place(*self.tcx, place.layout.ty)
};
let fn_abi = self.fn_abi_of_instance(instance, ty::List::empty())?;
let fn_abi = self.fn_abi_of_instance_no_deduced_attrs(instance, ty::List::empty())?;
let arg = self.mplace_to_ref(&place)?;
let ret = MPlaceTy::fake_alloc_zst(self.layout_of(self.tcx.types.unit)?);
@@ -153,16 +153,16 @@ pub fn fn_abi_of_fn_ptr(
}
/// This inherent method takes priority over the trait method with the same name in FnAbiOf,
/// and allows wrapping the actual [FnAbiOf::fn_abi_of_instance] with a tracing span.
/// See [FnAbiOf::fn_abi_of_instance] for the original documentation.
/// and allows wrapping the actual [FnAbiOf::fn_abi_of_instance_no_deduced_attrs] with a tracing span.
/// See [FnAbiOf::fn_abi_of_instance_no_deduced_attrs] for the original documentation.
#[inline(always)]
pub fn fn_abi_of_instance(
pub fn fn_abi_of_instance_no_deduced_attrs(
&self,
instance: ty::Instance<'tcx>,
extra_args: &'tcx ty::List<Ty<'tcx>>,
) -> <Self as FnAbiOfHelpers<'tcx>>::FnAbiOfResult {
let _trace = enter_trace_span!(M, layouting::fn_abi_of_instance, ?instance, ?extra_args);
FnAbiOf::fn_abi_of_instance(self, instance, extra_args)
FnAbiOf::fn_abi_of_instance_no_deduced_attrs(self, instance, extra_args)
}
}
@@ -470,7 +470,7 @@ fn eval_callee_and_args(
let instance = self.resolve(def_id, args)?;
(
FnVal::Instance(instance),
self.fn_abi_of_instance(instance, extra_args)?,
self.fn_abi_of_instance_no_deduced_attrs(instance, extra_args)?,
instance.def.requires_caller_location(*self.tcx),
)
}
+25 -4
View File
@@ -1801,11 +1801,32 @@
desc { "computing call ABI of `{}` function pointers", key.value.0 }
}
/// Compute a `FnAbi` suitable for declaring/defining an `fn` instance, and for
/// direct calls to an `fn`.
/// Compute a `FnAbi` suitable for declaring/defining an `fn` instance, and for direct calls*
/// to an `fn`. Indirectly-passed parameters in the returned ABI might not include all possible
/// codegen optimization attributes (such as `ReadOnly` or `CapturesNone`), as deducing these
/// requires inspection of function bodies that can lead to cycles when performed during typeck.
/// Post typeck, you should prefer the optimized ABI returned by `fn_abi_of_instance`.
///
/// NB: that includes virtual calls, which are represented by "direct calls"
/// to an `InstanceKind::Virtual` instance (of `<dyn Trait as Trait>::fn`).
/// NB: the ABI returned by this query must not differ from that returned by
/// `fn_abi_of_instance` in any other way.
///
/// * that includes virtual calls, which are represented by "direct calls" to an
/// `InstanceKind::Virtual` instance (of `<dyn Trait as Trait>::fn`).
query fn_abi_of_instance_no_deduced_attrs(
key: ty::PseudoCanonicalInput<'tcx, (ty::Instance<'tcx>, &'tcx ty::List<Ty<'tcx>>)>
) -> Result<&'tcx rustc_target::callconv::FnAbi<'tcx, Ty<'tcx>>, &'tcx ty::layout::FnAbiError<'tcx>> {
desc { "computing unadjusted call ABI of `{}`", key.value.0 }
}
/// Compute a `FnAbi` suitable for declaring/defining an `fn` instance, and for direct calls*
/// to an `fn`. Indirectly-passed parameters in the returned ABI will include applicable
/// codegen optimization attributes, including `ReadOnly` and `CapturesNone` -- deduction of
/// which requires inspection of function bodies that can lead to cycles when performed during
/// typeck. During typeck, you should therefore use instead the unoptimized ABI returned by
/// `fn_abi_of_instance_no_deduced_attrs`.
///
/// * that includes virtual calls, which are represented by "direct calls" to an
/// `InstanceKind::Virtual` instance (of `<dyn Trait as Trait>::fn`).
query fn_abi_of_instance(
key: ty::PseudoCanonicalInput<'tcx, (ty::Instance<'tcx>, &'tcx ty::List<Ty<'tcx>>)>
) -> Result<&'tcx rustc_target::callconv::FnAbi<'tcx, Ty<'tcx>>, &'tcx ty::layout::FnAbiError<'tcx>> {
+49 -4
View File
@@ -1369,11 +1369,56 @@ fn fn_abi_of_fn_ptr(
)
}
/// Compute a `FnAbi` suitable for declaring/defining an `fn` instance, and for
/// direct calls to an `fn`.
/// Compute a `FnAbi` suitable for declaring/defining an `fn` instance, and for direct calls*
/// to an `fn`. Indirectly-passed parameters in the returned ABI might not include all possible
/// codegen optimization attributes (such as `ReadOnly` or `CapturesNone`), as deducing these
/// requires inspection of function bodies that can lead to cycles when performed during typeck.
/// Post typeck, you should prefer the optimized ABI returned by `fn_abi_of_instance`.
///
/// NB: that includes virtual calls, which are represented by "direct calls"
/// to an `InstanceKind::Virtual` instance (of `<dyn Trait as Trait>::fn`).
/// NB: the ABI returned by this query must not differ from that returned by
/// `fn_abi_of_instance` in any other way.
///
/// * that includes virtual calls, which are represented by "direct calls" to an
/// `InstanceKind::Virtual` instance (of `<dyn Trait as Trait>::fn`).
#[inline]
#[tracing::instrument(level = "debug", skip(self))]
fn fn_abi_of_instance_no_deduced_attrs(
&self,
instance: ty::Instance<'tcx>,
extra_args: &'tcx ty::List<Ty<'tcx>>,
) -> Self::FnAbiOfResult {
// FIXME(eddyb) get a better `span` here.
let span = self.layout_tcx_at_span();
let tcx = self.tcx().at(span);
MaybeResult::from(
tcx.fn_abi_of_instance_no_deduced_attrs(
self.typing_env().as_query_input((instance, extra_args)),
)
.map_err(|err| {
// HACK(eddyb) at least for definitions of/calls to `Instance`s,
// we can get some kind of span even if one wasn't provided.
// However, we don't do this early in order to avoid calling
// `def_span` unconditionally (which may have a perf penalty).
let span = if !span.is_dummy() { span } else { tcx.def_span(instance.def_id()) };
self.handle_fn_abi_err(
*err,
span,
FnAbiRequest::OfInstance { instance, extra_args },
)
}),
)
}
/// Compute a `FnAbi` suitable for declaring/defining an `fn` instance, and for direct calls*
/// to an `fn`. Indirectly-passed parameters in the returned ABI will include applicable
/// codegen optimization attributes, including `ReadOnly` and `CapturesNone` -- deduction of
/// which requires inspection of function bodies that can lead to cycles when performed during
/// typeck. During typeck, you should therefore use instead the unoptimized ABI returned by
/// `fn_abi_of_instance_no_deduced_attrs`.
///
/// * that includes virtual calls, which are represented by "direct calls" to an
/// `InstanceKind::Virtual` instance (of `<dyn Trait as Trait>::fn`).
#[inline]
#[tracing::instrument(level = "debug", skip(self))]
fn fn_abi_of_instance(
+132 -66
View File
@@ -21,7 +21,12 @@
use tracing::debug;
pub(crate) fn provide(providers: &mut Providers) {
*providers = Providers { fn_abi_of_fn_ptr, fn_abi_of_instance, ..*providers };
*providers = Providers {
fn_abi_of_fn_ptr,
fn_abi_of_instance_no_deduced_attrs,
fn_abi_of_instance,
..*providers
};
}
// NOTE(eddyb) this is private to avoid using it from outside of
@@ -243,30 +248,95 @@ fn fn_sig_for_fn_abi<'tcx>(
}
}
/// Describes a function for determination of its ABI.
struct FnAbiDesc<'tcx> {
layout_cx: LayoutCx<'tcx>,
sig: ty::FnSig<'tcx>,
/// The function's definition, if its body can be used to deduce parameter attributes.
determined_fn_def_id: Option<DefId>,
caller_location: Option<Ty<'tcx>>,
is_virtual_call: bool,
extra_args: &'tcx [Ty<'tcx>],
}
impl<'tcx> FnAbiDesc<'tcx> {
fn for_fn_ptr(
tcx: TyCtxt<'tcx>,
query: ty::PseudoCanonicalInput<'tcx, (ty::PolyFnSig<'tcx>, &'tcx ty::List<Ty<'tcx>>)>,
) -> Self {
let ty::PseudoCanonicalInput { typing_env, value: (sig, extra_args) } = query;
Self {
layout_cx: LayoutCx::new(tcx, typing_env),
sig: tcx.normalize_erasing_regions(
typing_env,
tcx.instantiate_bound_regions_with_erased(sig),
),
// Parameter attributes can never be deduced for indirect calls, as there is no
// function body available to use.
determined_fn_def_id: None,
caller_location: None,
is_virtual_call: false,
extra_args,
}
}
fn for_instance(
tcx: TyCtxt<'tcx>,
query: ty::PseudoCanonicalInput<'tcx, (ty::Instance<'tcx>, &'tcx ty::List<Ty<'tcx>>)>,
) -> Self {
let ty::PseudoCanonicalInput { typing_env, value: (instance, extra_args) } = query;
let is_virtual_call = matches!(instance.def, ty::InstanceKind::Virtual(..));
let is_tls_shim_call = matches!(instance.def, ty::InstanceKind::ThreadLocalShim(_));
Self {
layout_cx: LayoutCx::new(tcx, typing_env),
sig: tcx.normalize_erasing_regions(
typing_env,
fn_sig_for_fn_abi(tcx, instance, typing_env),
),
// Parameter attributes can be deduced from the bodies of neither:
// - virtual calls, as they might call other functions from the vtable; nor
// - TLS shims, as they would refer to the underlying static.
determined_fn_def_id: (!is_virtual_call && !is_tls_shim_call)
.then(|| instance.def_id()),
caller_location: instance
.def
.requires_caller_location(tcx)
.then(|| tcx.caller_location_ty()),
is_virtual_call,
extra_args,
}
}
}
fn fn_abi_of_fn_ptr<'tcx>(
tcx: TyCtxt<'tcx>,
query: ty::PseudoCanonicalInput<'tcx, (ty::PolyFnSig<'tcx>, &'tcx ty::List<Ty<'tcx>>)>,
) -> Result<&'tcx FnAbi<'tcx, Ty<'tcx>>, &'tcx FnAbiError<'tcx>> {
let ty::PseudoCanonicalInput { typing_env, value: (sig, extra_args) } = query;
fn_abi_new_uncached(
&LayoutCx::new(tcx, typing_env),
tcx.instantiate_bound_regions_with_erased(sig),
extra_args,
None,
)
let desc = FnAbiDesc::for_fn_ptr(tcx, query);
fn_abi_new_uncached(desc)
}
fn fn_abi_of_instance_no_deduced_attrs<'tcx>(
tcx: TyCtxt<'tcx>,
query: ty::PseudoCanonicalInput<'tcx, (ty::Instance<'tcx>, &'tcx ty::List<Ty<'tcx>>)>,
) -> Result<&'tcx FnAbi<'tcx, Ty<'tcx>>, &'tcx FnAbiError<'tcx>> {
let desc = FnAbiDesc::for_instance(tcx, query);
fn_abi_new_uncached(desc)
}
fn fn_abi_of_instance<'tcx>(
tcx: TyCtxt<'tcx>,
query: ty::PseudoCanonicalInput<'tcx, (ty::Instance<'tcx>, &'tcx ty::List<Ty<'tcx>>)>,
) -> Result<&'tcx FnAbi<'tcx, Ty<'tcx>>, &'tcx FnAbiError<'tcx>> {
let ty::PseudoCanonicalInput { typing_env, value: (instance, extra_args) } = query;
fn_abi_new_uncached(
&LayoutCx::new(tcx, typing_env),
fn_sig_for_fn_abi(tcx, instance, typing_env),
extra_args,
Some(instance),
)
tcx.fn_abi_of_instance_no_deduced_attrs(query).map(|fn_abi| {
let params = FnAbiDesc::for_instance(tcx, query);
// If the function's body can be used to deduce parameter attributes, then adjust such
// "no deduced attrs" ABI; otherwise, return that ABI unadjusted.
params.determined_fn_def_id.map_or(fn_abi, |fn_def_id| {
fn_abi_adjust_for_deduced_attrs(&params.layout_cx, fn_abi, params.sig.abi, fn_def_id)
})
})
}
// Handle safe Rust thin and wide pointers.
@@ -483,27 +553,21 @@ fn fn_arg_sanity_check<'tcx>(
fn_arg_sanity_check(cx, fn_abi, spec_abi, &fn_abi.ret);
}
#[tracing::instrument(level = "debug", skip(cx, instance))]
#[tracing::instrument(
level = "debug",
skip(cx, caller_location, determined_fn_def_id, is_virtual_call)
)]
fn fn_abi_new_uncached<'tcx>(
cx: &LayoutCx<'tcx>,
sig: ty::FnSig<'tcx>,
extra_args: &[Ty<'tcx>],
instance: Option<ty::Instance<'tcx>>,
FnAbiDesc {
layout_cx: ref cx,
sig,
determined_fn_def_id,
caller_location,
is_virtual_call,
extra_args,
}: FnAbiDesc<'tcx>,
) -> Result<&'tcx FnAbi<'tcx, Ty<'tcx>>, &'tcx FnAbiError<'tcx>> {
let tcx = cx.tcx();
let (caller_location, determined_fn_def_id, is_virtual_call) = if let Some(instance) = instance
{
let is_virtual_call = matches!(instance.def, ty::InstanceKind::Virtual(..));
let is_tls_shim_call = matches!(instance.def, ty::InstanceKind::ThreadLocalShim(_));
(
instance.def.requires_caller_location(tcx).then(|| tcx.caller_location_ty()),
if is_virtual_call || is_tls_shim_call { None } else { Some(instance.def_id()) },
is_virtual_call,
)
} else {
(None, None, false)
};
let sig = tcx.normalize_erasing_regions(cx.typing_env, sig);
let abi_map = AbiMap::from_target(&tcx.sess.target);
let conv = abi_map.canonize_abi(sig.abi, sig.c_variadic).unwrap();
@@ -579,16 +643,7 @@ fn fn_abi_new_uncached<'tcx>(
sig.abi,
),
};
fn_abi_adjust_for_abi(
cx,
&mut fn_abi,
sig.abi,
// If this is a virtual call, we cannot pass the `fn_def_id`, as it might call other
// functions from vtable. And for a tls shim, passing the `fn_def_id` would refer to
// the underlying static. Internally, `deduced_param_attrs` attempts to infer attributes
// by visit the function body.
determined_fn_def_id,
);
fn_abi_adjust_for_abi(cx, &mut fn_abi, sig.abi);
debug!("fn_abi_new_uncached = {:?}", fn_abi);
fn_abi_sanity_check(cx, &fn_abi, sig.abi);
Ok(tcx.arena.alloc(fn_abi))
@@ -599,7 +654,6 @@ fn fn_abi_adjust_for_abi<'tcx>(
cx: &LayoutCx<'tcx>,
fn_abi: &mut FnAbi<'tcx, Ty<'tcx>>,
abi: ExternAbi,
fn_def_id: Option<DefId>,
) {
if abi == ExternAbi::Unadjusted {
// The "unadjusted" ABI passes aggregates in "direct" mode. That's fragile but needed for
@@ -620,34 +674,46 @@ fn unadjust<'tcx>(arg: &mut ArgAbi<'tcx, Ty<'tcx>>) {
for arg in fn_abi.args.iter_mut() {
unadjust(arg);
}
return;
}
let tcx = cx.tcx();
if abi.is_rustic_abi() {
} else if abi.is_rustic_abi() {
fn_abi.adjust_for_rust_abi(cx);
// Look up the deduced parameter attributes for this function, if we have its def ID and
// we're optimizing in non-incremental mode. We'll tag its parameters with those attributes
// as appropriate.
let deduced =
if tcx.sess.opts.optimize != OptLevel::No && tcx.sess.opts.incremental.is_none() {
fn_def_id.map(|fn_def_id| tcx.deduced_param_attrs(fn_def_id)).unwrap_or_default()
} else {
&[]
};
if !deduced.is_empty() {
apply_deduced_attributes(cx, deduced, 0, &mut fn_abi.ret);
for (arg_idx, arg) in fn_abi.args.iter_mut().enumerate() {
apply_deduced_attributes(cx, deduced, arg_idx + 1, arg);
}
}
} else {
fn_abi.adjust_for_foreign_abi(cx, abi);
}
}
#[tracing::instrument(level = "trace", skip(cx))]
fn fn_abi_adjust_for_deduced_attrs<'tcx>(
cx: &LayoutCx<'tcx>,
fn_abi: &'tcx FnAbi<'tcx, Ty<'tcx>>,
abi: ExternAbi,
fn_def_id: DefId,
) -> &'tcx FnAbi<'tcx, Ty<'tcx>> {
let tcx = cx.tcx();
// Look up the deduced parameter attributes for this function, if we have its def ID and
// we're optimizing in non-incremental mode. We'll tag its parameters with those attributes
// as appropriate.
let deduced = if abi.is_rustic_abi()
&& tcx.sess.opts.optimize != OptLevel::No
&& tcx.sess.opts.incremental.is_none()
{
tcx.deduced_param_attrs(fn_def_id)
} else {
&[]
};
if deduced.is_empty() {
fn_abi
} else {
let mut fn_abi = fn_abi.clone();
apply_deduced_attributes(cx, deduced, 0, &mut fn_abi.ret);
for (arg_idx, arg) in fn_abi.args.iter_mut().enumerate() {
apply_deduced_attributes(cx, deduced, arg_idx + 1, arg);
}
debug!("fn_abi_adjust_for_deduced_attrs = {:?}", fn_abi);
fn_abi_sanity_check(cx, &fn_abi, abi);
tcx.arena.alloc(fn_abi)
}
}
/// Apply deduced optimization attributes to a parameter using an indirect pass mode.
///
/// `deduced` is a possibly truncated list of deduced attributes for a return place and arguments.
@@ -0,0 +1,18 @@
//! Items whose type depends on CTFE (such as the async closure/coroutine beneath, whose type
//! depends upon evaluating `do_nothing`) should not cause a query cycle owing to the deduction of
//! the function's parameter attributes, which are only required for codegen and not for CTFE.
//!
//! Regression test for https://github.com/rust-lang/rust/issues/151748
//@ compile-flags: -O
//@ edition: 2018
//@ check-pass
fn main() {
let _ = async || {
let COMPLEX_CONSTANT = ();
};
}
const fn do_nothing() {}
const COMPLEX_CONSTANT: () = do_nothing();