Skip to content

Commit 897a658

Browse files
committed
interpret: change ABI-compat test to be type-based, so the test is consistent across targets
1 parent cd71a37 commit 897a658

File tree

2 files changed

+146
-68
lines changed

2 files changed

+146
-68
lines changed

compiler/rustc_const_eval/src/interpret/terminator.rs

Lines changed: 125 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,16 @@ use rustc_middle::{
66
mir,
77
ty::{
88
self,
9-
layout::{FnAbiOf, LayoutOf, TyAndLayout},
9+
layout::{FnAbiOf, IntegerExt, LayoutOf, TyAndLayout},
1010
Instance, Ty,
1111
},
1212
};
13-
use rustc_target::abi::call::{ArgAbi, FnAbi, PassMode};
14-
use rustc_target::abi::{self, FieldIdx};
13+
use rustc_span::sym;
14+
use rustc_target::abi::FieldIdx;
15+
use rustc_target::abi::{
16+
call::{ArgAbi, FnAbi, PassMode},
17+
Integer,
18+
};
1519
use rustc_target::spec::abi::Abi;
1620

1721
use super::{
@@ -255,6 +259,8 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
255259

256260
/// Find the wrapped inner type of a transparent wrapper.
257261
/// Must not be called on 1-ZST (as they don't have a uniquely defined "wrapped field").
262+
///
263+
/// We work with `TyAndLayout` here since that makes it much easier to iterate over all fields.
258264
fn unfold_transparent(&self, layout: TyAndLayout<'tcx>) -> TyAndLayout<'tcx> {
259265
match layout.ty.kind() {
260266
ty::Adt(adt_def, _) if adt_def.repr().transparent() => {
@@ -278,70 +284,124 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
278284
}
279285
}
280286

287+
/// Unwrap types that are guaranteed a null-pointer-optimization
288+
fn unfold_npo(&self, ty: Ty<'tcx>) -> InterpResult<'tcx, Ty<'tcx>> {
289+
// Check if this is `Option` wrapping some type.
290+
let inner_ty = match ty.kind() {
291+
ty::Adt(def, args) if self.tcx.is_diagnostic_item(sym::Option, def.did()) => {
292+
args[0].as_type().unwrap()
293+
}
294+
_ => {
295+
// Not an `Option`.
296+
return Ok(ty);
297+
}
298+
};
299+
// Check if the inner type is one of the NPO-guaranteed ones.
300+
Ok(match inner_ty.kind() {
301+
ty::Ref(..) => {
302+
// Option<&T> behaves like &T
303+
inner_ty
304+
}
305+
ty::Adt(def, _)
306+
if self.tcx.has_attr(def.did(), sym::rustc_nonnull_optimization_guaranteed) =>
307+
{
308+
// For non-null-guaranteed structs, unwrap newtypes.
309+
self.unfold_transparent(self.layout_of(inner_ty)?).ty
310+
}
311+
_ => {
312+
// Everything else we do not unfold.
313+
ty
314+
}
315+
})
316+
}
317+
281318
/// Check if these two layouts look like they are fn-ABI-compatible.
282319
/// (We also compare the `PassMode`, so this doesn't have to check everything. But it turns out
283320
/// that only checking the `PassMode` is insufficient.)
284321
fn layout_compat(
285322
&self,
286323
caller_layout: TyAndLayout<'tcx>,
287324
callee_layout: TyAndLayout<'tcx>,
288-
) -> bool {
325+
) -> InterpResult<'tcx, bool> {
326+
// Fast path: equal types are definitely compatible.
289327
if caller_layout.ty == callee_layout.ty {
290-
// Fast path: equal types are definitely compatible.
291-
return true;
328+
return Ok(true);
292329
}
293-
294-
match caller_layout.abi {
295-
// For Scalar/Vector/ScalarPair ABI, we directly compare them.
296-
// NOTE: this is *not* a stable guarantee! It just reflects a property of our current
297-
// ABIs. It's also fragile; the same pair of types might be considered ABI-compatible
298-
// when used directly by-value but not considered compatible as a struct field or array
299-
// element.
300-
abi::Abi::Scalar(..) | abi::Abi::ScalarPair(..) | abi::Abi::Vector { .. } => {
301-
caller_layout.abi.eq_up_to_validity(&callee_layout.abi)
302-
}
303-
_ => {
304-
// Everything else is compatible only if they newtype-wrap the same type, or if they are both 1-ZST.
305-
// (The latter part is needed to ensure e.g. that `struct Zst` is compatible with `struct Wrap((), Zst)`.)
306-
// This is conservative, but also means that our check isn't quite so heavily dependent on the `PassMode`,
307-
// which means having ABI-compatibility on one target is much more likely to imply compatibility for other targets.
308-
if caller_layout.is_1zst() || callee_layout.is_1zst() {
309-
// If either is a 1-ZST, both must be.
310-
caller_layout.is_1zst() && callee_layout.is_1zst()
311-
} else {
312-
// Neither is a 1-ZST, so we can check what they are wrapping.
313-
self.unfold_transparent(caller_layout).ty
314-
== self.unfold_transparent(callee_layout).ty
330+
// 1-ZST are compatible with all 1-ZST (and with nothing else).
331+
if caller_layout.is_1zst() || callee_layout.is_1zst() {
332+
return Ok(caller_layout.is_1zst() && callee_layout.is_1zst());
333+
}
334+
// Unfold newtypes and NPO optimizations.
335+
let caller_ty = self.unfold_npo(self.unfold_transparent(caller_layout).ty)?;
336+
let callee_ty = self.unfold_npo(self.unfold_transparent(callee_layout).ty)?;
337+
// Now see if these inner types are compatible.
338+
339+
// Compatible pointer types.
340+
let pointee_ty = |ty: Ty<'tcx>| {
341+
// We cannot use `builtin_deref` here since we need to reject `Box<T, MyAlloc>`.
342+
Some(match ty.kind() {
343+
ty::Ref(_, ty, _) => *ty,
344+
ty::RawPtr(mt) => mt.ty,
345+
// We should only accept `Box` with the default allocator.
346+
// It's hard to test for that though so we accept every 1-ZST allocator.
347+
ty::Adt(def, args)
348+
if def.is_box()
349+
&& self.layout_of(args[1].expect_ty()).is_ok_and(|l| l.is_1zst()) =>
350+
{
351+
args[0].expect_ty()
315352
}
316-
}
353+
_ => return None,
354+
})
355+
};
356+
if let (Some(left), Some(right)) = (pointee_ty(caller_ty), pointee_ty(callee_ty)) {
357+
// This is okay if they have the same metadata type.
358+
let meta_ty = |ty: Ty<'tcx>| {
359+
let (meta, only_if_sized) = ty.ptr_metadata_ty(*self.tcx, |ty| ty);
360+
assert!(
361+
!only_if_sized,
362+
"there should be no more 'maybe has that metadata' types during interpretation"
363+
);
364+
meta
365+
};
366+
return Ok(meta_ty(left) == meta_ty(right));
317367
}
368+
369+
// Compatible integer types (in particular, usize vs ptr-sized-u32/u64).
370+
let int_ty = |ty: Ty<'tcx>| {
371+
Some(match ty.kind() {
372+
ty::Int(ity) => (Integer::from_int_ty(&self.tcx, *ity), /* signed */ true),
373+
ty::Uint(uty) => (Integer::from_uint_ty(&self.tcx, *uty), /* signed */ false),
374+
_ => return None,
375+
})
376+
};
377+
if let (Some(left), Some(right)) = (int_ty(caller_ty), int_ty(callee_ty)) {
378+
// This is okay if they are the same integer type.
379+
return Ok(left == right);
380+
}
381+
382+
// Fall back to exact equality.
383+
// FIXME: We are missing the rules for "repr(C) wrapping compatible types".
384+
Ok(caller_ty == callee_ty)
318385
}
319386

320387
fn check_argument_compat(
321388
&self,
322389
caller_abi: &ArgAbi<'tcx, Ty<'tcx>>,
323390
callee_abi: &ArgAbi<'tcx, Ty<'tcx>>,
324-
) -> bool {
325-
// Ideally `PassMode` would capture everything there is about argument passing, but that is
326-
// not the case: in `FnAbi::llvm_type`, also parts of the layout and type information are
327-
// used. So we need to check that *both* sufficiently agree to ensures the arguments are
328-
// compatible.
329-
// For instance, `layout_compat` is needed to reject `i32` vs `f32`, which is not reflected
330-
// in `PassMode`. `mode_compat` is needed to reject `u8` vs `bool`, which have the same
331-
// `abi::Primitive` but different `arg_ext`.
332-
if self.layout_compat(caller_abi.layout, callee_abi.layout)
333-
&& caller_abi.mode.eq_abi(&callee_abi.mode)
334-
{
335-
// Something went very wrong if our checks don't imply layout ABI compatibility.
336-
assert!(caller_abi.layout.eq_abi(&callee_abi.layout));
337-
return true;
391+
) -> InterpResult<'tcx, bool> {
392+
// We do not want to accept things as ABI-compatible that just "happen to be" compatible on the current target,
393+
// so we implement a type-based check that reflects the guaranteed rules for ABI compatibility.
394+
if self.layout_compat(caller_abi.layout, callee_abi.layout)? {
395+
// Ensure that our checks imply actual ABI compatibility for this concrete call.
396+
assert!(caller_abi.eq_abi(&callee_abi));
397+
return Ok(true);
338398
} else {
339399
trace!(
340400
"check_argument_compat: incompatible ABIs:\ncaller: {:?}\ncallee: {:?}",
341401
caller_abi,
342402
callee_abi
343403
);
344-
return false;
404+
return Ok(false);
345405
}
346406
}
347407

@@ -360,6 +420,7 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
360420
'tcx: 'x,
361421
'tcx: 'y,
362422
{
423+
assert_eq!(callee_ty, callee_abi.layout.ty);
363424
if matches!(callee_abi.mode, PassMode::Ignore) {
364425
// This one is skipped. Still must be made live though!
365426
if !already_live {
@@ -371,8 +432,13 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
371432
let Some((caller_arg, caller_abi)) = caller_args.next() else {
372433
throw_ub_custom!(fluent::const_eval_not_enough_caller_args);
373434
};
435+
assert_eq!(caller_arg.layout().layout, caller_abi.layout.layout);
436+
// Sadly we cannot assert that `caller_arg.layout().ty` and `caller_abi.layout.ty` are
437+
// equal; in closures the types sometimes differ. We just hope that `caller_abi` is the
438+
// right type to print to the user.
439+
374440
// Check compatibility
375-
if !self.check_argument_compat(caller_abi, callee_abi) {
441+
if !self.check_argument_compat(caller_abi, callee_abi)? {
376442
let callee_ty = format!("{}", callee_ty);
377443
let caller_ty = format!("{}", caller_arg.layout().ty);
378444
throw_ub_custom!(
@@ -583,7 +649,7 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
583649
// taking into account the `spread_arg`. If we could write
584650
// this is a single iterator (that handles `spread_arg`), then
585651
// `pass_argument` would be the loop body. It takes care to
586-
// not advance `caller_iter` for ZSTs.
652+
// not advance `caller_iter` for ignored arguments.
587653
let mut callee_args_abis = callee_fn_abi.args.iter();
588654
for local in body.args_iter() {
589655
// Construct the destination place for this argument. At this point all
@@ -645,7 +711,7 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
645711
throw_ub_custom!(fluent::const_eval_too_many_caller_args);
646712
}
647713
// Don't forget to check the return type!
648-
if !self.check_argument_compat(&caller_fn_abi.ret, &callee_fn_abi.ret) {
714+
if !self.check_argument_compat(&caller_fn_abi.ret, &callee_fn_abi.ret)? {
649715
let callee_ty = format!("{}", callee_fn_abi.ret.layout.ty);
650716
let caller_ty = format!("{}", caller_fn_abi.ret.layout.ty);
651717
throw_ub_custom!(
@@ -674,7 +740,8 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
674740
Ok(()) => Ok(()),
675741
}
676742
}
677-
// cannot use the shim here, because that will only result in infinite recursion
743+
// `InstanceDef::Virtual` does not have callable MIR. Calls to `Virtual` instances must be
744+
// codegen'd / interpreted as virtual calls through the vtable.
678745
ty::InstanceDef::Virtual(def_id, idx) => {
679746
let mut args = args.to_vec();
680747
// We have to implement all "object safe receivers". So we have to go search for a
@@ -798,18 +865,26 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
798865
}
799866

800867
// Adjust receiver argument. Layout can be any (thin) ptr.
868+
let receiver_ty = Ty::new_mut_ptr(self.tcx.tcx, dyn_ty);
801869
args[0] = FnArg::Copy(
802870
ImmTy::from_immediate(
803871
Scalar::from_maybe_pointer(adjusted_receiver, self).into(),
804-
self.layout_of(Ty::new_mut_ptr(self.tcx.tcx, dyn_ty))?,
872+
self.layout_of(receiver_ty)?,
805873
)
806874
.into(),
807875
);
808876
trace!("Patched receiver operand to {:#?}", args[0]);
877+
// Need to also adjust the type in the ABI. Strangely, the layout there is actually
878+
// already fine! Just the type is bogus. This is due to what `force_thin_self_ptr`
879+
// does in `fn_abi_new_uncached`; supposedly, codegen relies on having the bogus
880+
// type, so we just patch this up locally.
881+
let mut caller_fn_abi = caller_fn_abi.clone();
882+
caller_fn_abi.args[0].layout.ty = receiver_ty;
883+
809884
// recurse with concrete function
810885
self.eval_fn_call(
811886
FnVal::Instance(fn_inst),
812-
(caller_abi, caller_fn_abi),
887+
(caller_abi, &caller_fn_abi),
813888
&args,
814889
with_caller_location,
815890
destination,

src/tools/miri/tests/pass/function_calls/abi_compat.rs

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
use std::mem;
22
use std::num;
3+
use std::ptr;
34

45
#[derive(Copy, Clone, Default)]
56
struct Zst;
67

7-
fn test_abi_compat<T: Copy, U: Copy>(t: T, u: U) {
8+
fn test_abi_compat<T: Clone, U: Clone>(t: T, u: U) {
89
fn id<T>(x: T) -> T {
910
x
1011
}
@@ -16,10 +17,10 @@ fn test_abi_compat<T: Copy, U: Copy>(t: T, u: U) {
1617
// in both directions.
1718
let f: fn(T) -> T = id;
1819
let f: fn(U) -> U = unsafe { std::mem::transmute(f) };
19-
let _val = f(u);
20+
let _val = f(u.clone());
2021
let f: fn(U) -> U = id;
2122
let f: fn(T) -> T = unsafe { std::mem::transmute(f) };
22-
let _val = f(t);
23+
let _val = f(t.clone());
2324

2425
// And then we do the same for `extern "C"`.
2526
let f: extern "C" fn(T) -> T = id_c;
@@ -54,23 +55,25 @@ fn test_abi_newtype<T: Copy + Default>() {
5455
}
5556

5657
fn main() {
57-
// Here we check:
58-
// - u32 vs char is allowed
59-
// - u32 vs NonZeroU32/Option<NonZeroU32> is allowed
60-
// - reference vs raw pointer is allowed
61-
// - references to things of the same size and alignment are allowed
62-
// These are very basic tests that should work on all ABIs. However it is not clear that any of
63-
// these would be stably guaranteed. Code that relies on this is equivalent to code that relies
64-
// on the layout of `repr(Rust)` types. They are also fragile: the same mismatches in the fields
65-
// of a struct (even with `repr(C)`) will not always be accepted by Miri.
66-
// Note that `bool` and `u8` are *not* compatible, at least on x86-64!
67-
// One of them has `arg_ext: Zext`, the other does not.
68-
// Similarly, `i32` and `u32` are not compatible on s390x due to different `arg_ext`.
69-
test_abi_compat(0u32, 'x');
70-
test_abi_compat(42u32, num::NonZeroU32::new(1).unwrap());
71-
test_abi_compat(0u32, Some(num::NonZeroU32::new(1).unwrap()));
58+
// Here we check some of the guaranteed ABI compatibilities.
59+
// Different integer types of the same size and sign.
60+
if cfg!(target_pointer_width = "32") {
61+
test_abi_compat(0usize, 0u32);
62+
test_abi_compat(0isize, 0i32);
63+
} else {
64+
test_abi_compat(0usize, 0u64);
65+
test_abi_compat(0isize, 0i64);
66+
}
67+
// Reference/pointer types with the same pointee.
7268
test_abi_compat(&0u32, &0u32 as *const u32);
69+
test_abi_compat(&mut 0u32 as *mut u32, Box::new(0u32));
70+
test_abi_compat(&(), ptr::NonNull::<()>::dangling());
71+
// Reference/pointer types with different but sized pointees.
7372
test_abi_compat(&0u32, &([true; 4], [0u32; 0]));
73+
// Guaranteed null-pointer-optimizations.
74+
test_abi_compat(&0u32 as *const u32, Some(&0u32));
75+
test_abi_compat(42u32, num::NonZeroU32::new(1).unwrap());
76+
test_abi_compat(0u32, Some(num::NonZeroU32::new(1).unwrap()));
7477

7578
// These must work for *any* type, since we guarantee that `repr(transparent)` is ABI-compatible
7679
// with the wrapped field.

0 commit comments

Comments
 (0)