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
10 changes: 6 additions & 4 deletions compiler/rustc_ast/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3059,21 +3059,23 @@ impl FnDecl {
}

/// The marker index for "no splatted arguments".
/// Higher values are also not supported, for performance reasons.
///
/// Must have the same value as `FnSigKind::NO_SPLATTED_ARG_INDEX` and `FnDeclFlags::NO_SPLATTED_ARG_INDEX`.
pub const NO_SPLATTED_ARG_INDEX: u16 = u16::MAX;
pub const NO_SPLATTED_ARG_INDEX: u8 = u8::MAX;

/// Returns a splatted argument index, if any are present.
pub fn splatted(&self) -> Option<u16> {
pub fn splatted(&self) -> Option<u8> {
self.inputs.iter().enumerate().find_map(|(index, arg)| {
if index == Self::NO_SPLATTED_ARG_INDEX as usize {
if index >= usize::from(Self::NO_SPLATTED_ARG_INDEX) {
// AST validation has already checked the splatted argument index is valid, so just
// ignore invalid indexes here.
None
} else {
arg.attrs
.iter()
.any(|attr| attr.has_name(sym::splat))
.then_some(u16::try_from(index).unwrap())
.then_some(u8::try_from(index).unwrap())
}
})
}
Expand Down
5 changes: 2 additions & 3 deletions compiler/rustc_ast_lowering/src/delegation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ struct ParamInfo {
pub c_variadic: bool,

/// The index of the splatted parameter, if any.
pub splatted: Option<u16>,
pub splatted: Option<u8>,
}

const PARENT_ID: hir::ItemLocalId = hir::ItemLocalId::ZERO;
Expand Down Expand Up @@ -364,11 +364,10 @@ impl<'hir> LoweringContext<'_, 'hir> {
fn param_info(&self, def_id: DefId) -> ParamInfo {
let sig = self.tcx.fn_sig(def_id).skip_binder().skip_binder();

// FIXME(splat): use `sig.splatted()` once FnSig has it
ParamInfo {
param_count: sig.inputs().len() + usize::from(sig.c_variadic()),
c_variadic: sig.c_variadic(),
splatted: None,
splatted: sig.splatted(),
}
}

Expand Down
6 changes: 3 additions & 3 deletions compiler/rustc_ast_passes/src/ast_validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -412,11 +412,11 @@ impl<'a> AstValidator<'a> {
})
.unzip();

// A splatted argument at the "no splatted" marker index is not supported (this is an
// unlikely edge case).
// A splatted argument greater than or equal to the "no splatted" marker index is not
// supported.
if let (Some(&splatted_arg_index), Some(&splatted_span)) =
(splatted_arg_indexes.last(), splatted_spans.last())
&& splatted_arg_index == FnDecl::NO_SPLATTED_ARG_INDEX
&& splatted_arg_index >= u16::from(FnDecl::NO_SPLATTED_ARG_INDEX)
{
self.dcx().emit_err(diagnostics::InvalidSplattedArg {
splatted_arg_index,
Expand Down
1 change: 1 addition & 0 deletions compiler/rustc_codegen_cranelift/src/abi/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ pub(crate) fn codegen_fn_prelude<'tcx>(fx: &mut FunctionCx<'_, '_, 'tcx>, start_
.map(|local| {
let arg_ty = fx.monomorphize(fx.mir.local_decls[local].ty);

// FIXME(splat): un-tuple splatted arguments in codegen, for performance
// Adapted from https://github.com/rust-lang/rust/blob/145155dc96757002c7b2e9de8489416e2fdbbd57/src/librustc_codegen_llvm/mir/mod.rs#L442-L482
if Some(local) == fx.mir.spread_arg {
// This argument (e.g. the last argument in the "rust-call" ABI)
Expand Down
1 change: 1 addition & 0 deletions compiler/rustc_codegen_ssa/src/debuginfo/type_names.rs
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,7 @@ fn push_debuginfo_type_name<'tcx>(
output.push_str("fn(");
}

// FIXME(splat): should debuginfo be de-tupled in the callee (and caller)?
if !sig.inputs().is_empty() {
for &parameter_type in sig.inputs() {
push_debuginfo_type_name(tcx, parameter_type, true, output, visited);
Expand Down
1 change: 1 addition & 0 deletions compiler/rustc_codegen_ssa/src/mir/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1202,6 +1202,7 @@ impl<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>> FunctionCx<'a, 'tcx, Bx> {
};

// Split the rust-call tupled arguments off.
// FIXME(splat): un-tuple splatted arguments in codegen, for performance
Comment thread
teor2345 marked this conversation as resolved.

@RalfJung RalfJung Apr 28, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why "for performance"? If this follows the RustCall approach, the ABI requires untupling for correctness. I am confused by this comment.

View changes since the review

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Oh I see, I think I got confused by "splatted MIR lowering and tupling". This doesn't actually do anything on the ABI level yet, it passes things as structs.

let (first_args, untuple) = if sig.abi() == ExternAbi::RustCall
&& let Some((tup, args)) = args.split_last()
{
Expand Down
1 change: 1 addition & 0 deletions compiler/rustc_codegen_ssa/src/mir/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,7 @@ fn arg_local_refs<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>>(
let arg_decl = &mir.local_decls[local];
let arg_ty = fx.monomorphize(arg_decl.ty);

// FIXME(splat): re-tuple splatted arguments that were un-tupled in the ABI
if Some(local) == mir.spread_arg {
// This argument (e.g., the last argument in the "rust-call" ABI)
// is a tuple that was spread at the ABI level and now we have
Expand Down
18 changes: 17 additions & 1 deletion compiler/rustc_const_eval/src/const_eval/type_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use rustc_ast::Mutability;
use rustc_hir::LangItem;
use rustc_middle::span_bug;
use rustc_middle::ty::layout::TyAndLayout;
use rustc_middle::ty::{self, Const, FnHeader, FnSigTys, ScalarInt, Ty, TyCtxt};
use rustc_middle::ty::{self, Const, FnHeader, FnSigKind, FnSigTys, ScalarInt, Ty, TyCtxt};
use rustc_span::{Symbol, sym};

use crate::const_eval::CompileTimeMachine;
Expand Down Expand Up @@ -436,6 +436,22 @@ impl<'tcx> InterpCx<'tcx, CompileTimeMachine<'tcx>> {
sym::variadic => {
self.write_scalar(Scalar::from_bool(fn_sig_kind.c_variadic()), &field_place)?;
}
sym::is_splatted => {
self.write_scalar(
Scalar::from_bool(fn_sig_kind.splatted().is_some()),
&field_place,
)?;
}
sym::splatted_index => {
self.write_scalar(
Scalar::from_u8(
// Currently the same encoding as FnSigKind.splatted
// FIXME(splat): make these two fields into a single Option<u8/u16>, or choose a stable encoding
fn_sig_kind.splatted().unwrap_or(FnSigKind::NO_SPLATTED_ARG_INDEX),
),
&field_place,
)?;
}
other => span_bug!(self.tcx.def_span(field.did), "unimplemented field {other}"),
}
}
Expand Down
1 change: 1 addition & 0 deletions compiler/rustc_const_eval/src/interpret/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,7 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
};

// Special handling for the closure ABI: untuple the last argument.
// FIXME(splat): un-tuple splatted arguments that were tupled in typecheck

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Isn't this meant to be desugared already during MIR building? That seems preferable. There's only one place where we do MIR building, but there's 3-4 MIR consumers in-tree depending on how you count, and a bunch more out-of-tree.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Now that I re-read the discussion, that makes the most sense:
#t-lang > On overloading @ 💬

I think I can de-tuple in MIR. It's a bit more complicated because it involves de-tupling both the caller and callee. But the code might end up nicer, because we'd just be splatting whatever's inside the tuple. So there's no messing around with different argument counts.

Did you want that change in this PR?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Note that MIR is generic, so this will only work if splatting doesn't support "generic forwarding". Is that meant to be possible?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure, the kind of overloading we're aiming for is in this UI test:
https://github.com/rust-lang/rust/pull/153697/changes#diff-c3e596dfdd17cabcfcaca5dd69bd8b1e1f5a43bc19fd0ab26d23d010dae596b2R12

I don't know if we'd want generic forwarding for more complex overloading situations.

Would we need it to support custom C++ pointer types? Or any other interop features?

Crubit wants custom auto traits eventually, they're currently using a combination of generated impls and blanket impls.

(Maybe this is a broader lang + interop question.)

@programmerjake programmerjake Mar 27, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

assuming we want splatting to replace extern "rust-call", we'll need generic forwarding for things like: impl Fn<Args> for Box<F> https://doc.rust-lang.org/1.93.1/src/alloc/boxed.rs.html#2219

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Would we need it to support custom C++ pointer types? Or any other interop features?

I don't know. I haven't been following in detail.

I think I also misunderstood what splat does. That ui test looks more like it's... unsplatting? Like, the caller passes a bunch of separate arguments, but actually those become a tuple and then the callee just sees the tuple. Is the idea that on the ABI level these are a single big tuple or separate arguments or does it not matter?

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.

imo it's up to the backend, but an optimizing backend should untuple the arguments

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.

hmm @RalfJung @workingjubilee since this is a thing that solely exists for the Rust ABI, should this just be a field on ExternAbi::Rust ?

@RalfJung RalfJung Mar 28, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I wonder if this can't just reuse the same logic as what RustCall already uses? It seems to have exactly the behavior we want -- in fact it's already more powerful than we need: with RustCall, caller and callee can disagree on whether splatting is happening. RustCall is really two features:

  • A caller-side transformation where, for ABI purposes, the last argument is a tuple that has its fields passed as separate arguments. So we may have a function with signature fn(i32, i64, f64, i32) and then we can call it via fn(i32, (i64, f64, i32)) but also via fn(i32, i64, (f64, if32)). Both generate the same actual call. This happens post-mono, i.e. the tuple can even be generic.
  • A callee-side transformation where one MIR local can be designated as a "spread arg". That local must have tuple type, and it will then gather N arguments that are separate on the ABI into the fields of that local. (The interpreter allows more non-spread args after this, but I don't know if codegen also supports this.) This is independent of whether the caller used "untupling" since the ABI actually has separate arguments here. The type of the local can be generic (but it must then only ever be instantiated as a tuple type); all the actual handling happens post-mono.

let args: Cow<'_, [FnArg<'tcx, M::Provenance>]> =
if caller_abi == ExternAbi::RustCall && !args.is_empty() {
// Untuple
Expand Down
41 changes: 21 additions & 20 deletions compiler/rustc_hir/src/hir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4045,12 +4045,12 @@ pub struct Param<'hir> {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SplattedArgIndexError {
/// The splatted argument index is invalid.
/// Currently the only unsupported index is `u16::MAX`, which is used to indicate that no argument
/// is splatted.
InvalidIndex { splatted_arg_index: u16 },
/// A `u8::MAX` argument index used to indicate that no argument is splatted.
/// Higher values are also not supported, for performance reasons.
InvalidIndex { splatted_arg_index: u8 },

/// The splatted argument index is outside the bounds of the function arguments.
OutOfBounds { splatted_arg_index: u16, args_len: u16 },
OutOfBounds { splatted_arg_index: u8, args_len: u16 },
}

/// Contains the packed non-type fields of a function declaration.
Expand All @@ -4061,9 +4061,9 @@ pub struct FnDeclFlags {
flags: u8,

/// Which function argument is splatted into multiple arguments in callers, if any?
/// Splatting functions with `u16::MAX` arguments is not supported, see `FnSigKind` for
/// Splatting functions with `>= u8::MAX` arguments is not supported, see `FnSigKind` for
/// details.
splatted: u16,
splatted: u8,
}

impl fmt::Debug for FnDeclFlags {
Expand Down Expand Up @@ -4101,13 +4101,13 @@ impl FnDeclFlags {

/// Marker index for "no splatted argument".
/// Must have the same value as `FnSigKind::NO_SPLATTED_ARG_INDEX` and `rustc_ast::FnDecl::NO_SPLATTED_ARG_INDEX`.
const NO_SPLATTED_ARG_INDEX: u16 = u16::MAX;
const NO_SPLATTED_ARG_INDEX: u8 = u8::MAX;

/// Create a new FnDeclKind with no implicit self, no lifetime elision, no C-style variadic
/// argument, and no splatting.
/// To modify these flags, use the `set_*` methods, for readability.
// FIXME: use Default instead when that trait is const stable.
pub const fn default() -> Self {
pub fn default() -> Self {
Comment thread
oli-obk marked this conversation as resolved.
Self { flags: 0, splatted: 0 }
.set_implicit_self(ImplicitSelfKind::None)
.set_lifetime_elision_allowed(false)
Expand All @@ -4117,7 +4117,7 @@ impl FnDeclFlags {

/// Set the implicit self kind.
#[must_use = "this method does not modify the receiver"]
pub const fn set_implicit_self(mut self, implicit_self: ImplicitSelfKind) -> Self {
pub fn set_implicit_self(mut self, implicit_self: ImplicitSelfKind) -> Self {
self.flags &= !Self::IMPLICIT_SELF_MASK;

match implicit_self {
Expand All @@ -4133,7 +4133,7 @@ impl FnDeclFlags {

/// Set the C-style variadic argument flag.
#[must_use = "this method does not modify the receiver"]
pub const fn set_c_variadic(mut self, c_variadic: bool) -> Self {
pub fn set_c_variadic(mut self, c_variadic: bool) -> Self {
if c_variadic {
self.flags |= Self::C_VARIADIC_FLAG;
} else {
Expand All @@ -4145,7 +4145,7 @@ impl FnDeclFlags {

/// Set the lifetime elision allowed flag.
#[must_use = "this method does not modify the receiver"]
pub const fn set_lifetime_elision_allowed(mut self, allowed: bool) -> Self {
pub fn set_lifetime_elision_allowed(mut self, allowed: bool) -> Self {
if allowed {
self.flags |= Self::LIFETIME_ELISION_ALLOWED_FLAG;
} else {
Expand All @@ -4158,16 +4158,17 @@ impl FnDeclFlags {
/// Set the splatted argument index.
/// The number of function arguments is used for error checking.
#[must_use = "this method does not modify the receiver"]
pub const fn set_splatted(
pub fn set_splatted(
mut self,
splatted: Option<u16>,
splatted: Option<u8>,
args_len: usize,
) -> Result<Self, SplattedArgIndexError> {
if let Some(splatted_arg_index) = splatted {
if splatted_arg_index == Self::NO_SPLATTED_ARG_INDEX {
// This index value is used as a marker for "no splatting", so it is unsupported.
// Higher values are also not supported, for performance reasons.
return Err(SplattedArgIndexError::InvalidIndex { splatted_arg_index });
} else if splatted_arg_index as usize >= args_len {
} else if usize::from(splatted_arg_index) >= args_len {
return Err(SplattedArgIndexError::OutOfBounds {
splatted_arg_index,
args_len: args_len as u16,
Expand All @@ -4184,14 +4185,14 @@ impl FnDeclFlags {

/// Set "no splatted arguments" for the function declaration.
#[must_use = "this method does not modify the receiver"]
pub const fn set_no_splatted_args(mut self) -> Self {
pub fn set_no_splatted_args(mut self) -> Self {
self.splatted = Self::NO_SPLATTED_ARG_INDEX;

self
}

/// Get the implicit self kind.
pub const fn implicit_self(self) -> ImplicitSelfKind {
pub fn implicit_self(self) -> ImplicitSelfKind {
match self.flags & Self::IMPLICIT_SELF_MASK {
0 => ImplicitSelfKind::None,
1 => ImplicitSelfKind::Imm,
Expand All @@ -4203,17 +4204,17 @@ impl FnDeclFlags {
}

/// Do the function arguments end with a C-style variadic argument?
pub const fn c_variadic(self) -> bool {
pub fn c_variadic(self) -> bool {
self.flags & Self::C_VARIADIC_FLAG != 0
}

/// Is lifetime elision allowed?
pub const fn lifetime_elision_allowed(self) -> bool {
pub fn lifetime_elision_allowed(self) -> bool {
self.flags & Self::LIFETIME_ELISION_ALLOWED_FLAG != 0
}

/// Get the splatted argument index, if any.
pub const fn splatted(self) -> Option<u16> {
pub fn splatted(self) -> Option<u8> {
if self.splatted == Self::NO_SPLATTED_ARG_INDEX { None } else { Some(self.splatted) }
}
}
Expand Down Expand Up @@ -4263,7 +4264,7 @@ impl<'hir> FnDecl<'hir> {
self.fn_decl_kind.lifetime_elision_allowed()
}

pub fn splatted(&self) -> Option<u16> {
pub fn splatted(&self) -> Option<u8> {
self.fn_decl_kind.splatted()
}

Expand Down
4 changes: 3 additions & 1 deletion compiler/rustc_hir_analysis/src/check/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -444,11 +444,13 @@ fn fn_sig_suggestion<'tcx>(
predicates: impl IntoIterator<Item = (ty::Clause<'tcx>, Span)>,
assoc: ty::AssocItem,
) -> String {
let splatted_arg_index = sig.splatted().map(usize::from);
let args = sig
.inputs()
.iter()
.enumerate()
.map(|(i, ty)| {
let splat = if splatted_arg_index == Some(i) { "#[splat] " } else { "" };
let arg_ty = match ty.kind() {
ty::Param(_) if assoc.is_method() && i == 0 => "self".to_string(),
ty::Ref(reg, ref_ty, mutability) if i == 0 => {
Expand Down Expand Up @@ -477,7 +479,7 @@ fn fn_sig_suggestion<'tcx>(
}
}
};
Some(format!("{arg_ty}"))
Some(format!("{splat}{arg_ty}"))
})
.chain(std::iter::once(if sig.c_variadic() { Some("...".to_string()) } else { None }))
.flatten()
Expand Down
11 changes: 10 additions & 1 deletion compiler/rustc_hir_analysis/src/check/wfcheck.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1714,7 +1714,16 @@ fn check_fn_or_method<'tcx>(
let span = tcx.def_span(def_id);
let has_implicit_self = hir_decl.implicit_self().has_implicit_self();
let mut inputs = sig.inputs().iter().skip(if has_implicit_self { 1 } else { 0 });
// FIXME(splat): use `sig.splatted()` once FnSig has it
// FIXME(splat): support the rest of closure splatting, or replace this code with an error
if let Some(mut splatted_arg_index) = sig.splatted() {
let mut inputs_count = sig.inputs().len();
if has_implicit_self {
splatted_arg_index = splatted_arg_index.strict_sub(1);
inputs_count = inputs_count.strict_sub(1);
}
debug!(?splatted_arg_index, ?inputs_count, ?has_implicit_self, ?sig);
sig = sig.set_splatted(Some(splatted_arg_index), inputs_count).unwrap();
}
// Check that the argument is a tuple and is sized
if let Some(ty) = inputs.next() {
wfcx.register_bound(
Expand Down
5 changes: 3 additions & 2 deletions compiler/rustc_hir_analysis/src/hir_ty_lowering/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3582,11 +3582,12 @@ impl<'tcx> dyn HirTyLowerer<'tcx> + '_ {
debug!(?output_ty);

debug!(?abi, ?safety, ?decl.fn_decl_kind, input_tys_len = ?input_tys.len());
// FIXME(splat): use `set_splatted()` once FnSig has it
let fn_sig_kind = FnSigKind::default()
.set_abi(abi)
.set_safety(safety)
.set_c_variadic(decl.fn_decl_kind.c_variadic());
.set_c_variadic(decl.fn_decl_kind.c_variadic())
.set_splatted(decl.splatted(), input_tys.len())
.unwrap();
let fn_ty = tcx.mk_fn_sig(input_tys, output_ty, fn_sig_kind);
let fn_ptr_ty = ty::Binder::bind_with_vars(fn_ty, bound_vars);

Expand Down
Loading
Loading