Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
7cfa4d7
add `GlobalAlloc::VaList`
folkertdev Jan 2, 2026
f237efc
allow `const fn` to be c-variadic
folkertdev Jan 2, 2026
1651931
make `Va::arg` and `VaList::drop` `const fn`s
folkertdev Jan 2, 2026
ab1918f
on call, split the standard and c-variadic arguments
folkertdev Jan 2, 2026
36759a2
set up `VaList` global storage
folkertdev Jan 2, 2026
ce45670
implement `va_arg` in `rustc_const_eval`
folkertdev Jan 2, 2026
2ca4269
basic support for `AllocKind::VaList` in miri
folkertdev Jan 2, 2026
5d20e76
add c-variadic const eval test
folkertdev Jan 2, 2026
25e96ba
basic `va_copy` implementation
folkertdev Jan 7, 2026
c0ce0b0
move over to the shared interpreter
folkertdev Jan 8, 2026
9e57c73
more copy UB tests
folkertdev Jan 20, 2026
67aabb2
stop using `reserve_and_set_va_list_alloc`
folkertdev Jan 22, 2026
bdc0935
make `va_list_map` private
folkertdev Jan 22, 2026
83db12a
remove `GlobalAlloc::VaList` again
folkertdev Jan 22, 2026
902fd15
add (dead) allocation bookkeeping
folkertdev Jan 22, 2026
ab52d77
zero the `VaList` place so it is initialized
folkertdev Jan 22, 2026
33fcb97
add c-variadic miri test
folkertdev Jan 22, 2026
1bcf89f
WIP
folkertdev Jan 22, 2026
a564f24
move va_list operations into `InterpCx`
folkertdev Jan 23, 2026
76b68b1
`fn addr_from_alloc_id_uncached`: make va_list emit a dummy_alloc
folkertdev Jan 24, 2026
14fa6a8
use places smarter
folkertdev Jan 24, 2026
1c86e0b
move things around
folkertdev Jan 24, 2026
023a038
fix formatting
folkertdev Jan 24, 2026
666adc8
rename to `self.va_list_ptr`
folkertdev Jan 24, 2026
71af2d1
add more miri tests
folkertdev Jan 24, 2026
8ad00dc
Apply suggestion from @RalfJung
folkertdev Jan 25, 2026
0e7c74e
low-hanging fruit after review
folkertdev Jan 25, 2026
0b084d7
consider the caller location argument
folkertdev Jan 25, 2026
e6acf30
use `transmute_copy`
folkertdev Jan 25, 2026
ba2ba3a
split out `allocate_varargs`
folkertdev Jan 25, 2026
7a02aa1
report UB when caller and callee signatures do not match
folkertdev Jan 28, 2026
e8cda1f
retrieve key field without the lang item
folkertdev Jan 28, 2026
db1bee2
feature-gate c-variadic definitions and calls in const contexts
folkertdev Jan 28, 2026
e4bc4f9
clean up feature gate
folkertdev Jan 29, 2026
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
4 changes: 0 additions & 4 deletions compiler/rustc_ast_passes/messages.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,6 @@ ast_passes_c_variadic_no_extern = `...` is not supported for non-extern function

ast_passes_c_variadic_not_supported = the `{$target}` target does not support c-variadic functions

ast_passes_const_and_c_variadic = functions cannot be both `const` and C-variadic
.const = `const` because of this
.variadic = C-variadic because of this

ast_passes_const_and_coroutine = functions cannot be both `const` and `{$coroutine_kind}`
.const = `const` because of this
.coroutine = `{$coroutine_kind}` because of this
Expand Down
12 changes: 5 additions & 7 deletions compiler/rustc_ast_passes/src/ast_validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -697,13 +697,11 @@ impl<'a> AstValidator<'a> {
unreachable!("C variable argument list cannot be used in closures")
};

// C-variadics are not yet implemented in const evaluation.
if let Const::Yes(const_span) = sig.header.constness {
Copy link
Member

Choose a reason for hiding this comment

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

Do we have tests ensuring that c-variadics in const remain unstable?

The usual place that would be enforces is compiler/rustc_const_eval/src/check_consts/mod.rs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This should get its own feature gate then? Because this PR also includes const c-variadic calls, and non-const c-variadic calls are already stable.

Copy link
Member

Choose a reason for hiding this comment

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

A new feature gate sounds good. That should then cover both declaring variadic const fn and calling them from inside const contexts. (Those could also be different feature gates but that doesn't seem necessary.)

self.dcx().emit_err(errors::ConstAndCVariadic {
spans: vec![const_span, variadic_param.span],
const_span,
variadic_span: variadic_param.span,
});
if let Const::Yes(_) = sig.header.constness
&& !self.features.enabled(sym::const_c_variadic)
{
let msg = format!("c-variadic const function definitions are unstable");
feature_err(&self.sess, sym::const_c_variadic, sig.span, msg).emit();
}

if let Some(coroutine_kind) = sig.header.coroutine_kind {
Expand Down
11 changes: 0 additions & 11 deletions compiler/rustc_ast_passes/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -710,17 +710,6 @@ pub(crate) struct ConstAndCoroutine {
pub coroutine_kind: &'static str,
}

#[derive(Diagnostic)]
#[diag(ast_passes_const_and_c_variadic)]
pub(crate) struct ConstAndCVariadic {
#[primary_span]
pub spans: Vec<Span>,
#[label(ast_passes_const)]
pub const_span: Span,
#[label(ast_passes_variadic)]
pub variadic_span: Span,
}

#[derive(Diagnostic)]
#[diag(ast_passes_coroutine_and_c_variadic)]
pub(crate) struct CoroutineAndCVariadic {
Expand Down
17 changes: 17 additions & 0 deletions compiler/rustc_const_eval/messages.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@ const_eval_bad_pointer_op_attempting = {const_eval_bad_pointer_op}: {$operation

const_eval_bounds_check_failed =
indexing out of bounds: the len is {$len} but the index is {$index}

const_eval_c_variadic_call =
calling const c-variadic functions is unstable in {const_eval_const_context}s

const_eval_c_variadic_fixed_count_mismatch =
calling a C-variadic function with {$caller} fixed arguments, but the function expects {$callee}

const_eval_c_variadic_mismatch =
calling a function where the caller and callee disagree on whether the function is C-variadic

const_eval_call_nonzero_intrinsic =
`{$name}` called on 0

Expand Down Expand Up @@ -91,6 +101,8 @@ const_eval_deref_function_pointer =
accessing {$allocation} which contains a function
const_eval_deref_typeid_pointer =
accessing {$allocation} which contains a `TypeId`
const_eval_deref_va_list_pointer =
accessing {$allocation} which contains a variable argument list
const_eval_deref_vtable_pointer =
accessing {$allocation} which contains a vtable
const_eval_division_by_zero =
Expand Down Expand Up @@ -195,6 +207,9 @@ const_eval_invalid_uninit_bytes =
const_eval_invalid_uninit_bytes_unknown =
using uninitialized data, but this operation requires initialized memory

const_eval_invalid_va_list_pointer =
using {$pointer} as variable argument list pointer but it does not point to a variable argument list

const_eval_invalid_vtable_pointer =
using {$pointer} as vtable pointer but it does not point to a vtable

Expand Down Expand Up @@ -430,6 +445,8 @@ const_eval_unterminated_c_string =
const_eval_unwind_past_top =
unwinding past the topmost frame of the stack

const_eval_va_arg_out_of_bounds = more C-variadic arguments read than were passed

## The `front_matter`s here refer to either `const_eval_front_matter_invalid_value` or `const_eval_front_matter_invalid_value_with_path`.
## (We'd love to sort this differently to make that more clear but tidy won't let us...)
const_eval_validation_box_to_uninhabited = {$front_matter}: encountered a box pointing to uninhabited type {$ty}
Expand Down
4 changes: 4 additions & 0 deletions compiler/rustc_const_eval/src/check_consts/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -815,6 +815,10 @@ impl<'tcx> Visitor<'tcx> for Checker<'_, 'tcx> {
});
}

if self.tcx.fn_sig(callee).skip_binder().c_variadic() {
self.check_op(ops::FnCallCVariadic)
}

// At this point, we are calling a function, `callee`, whose `DefId` is known...

// `begin_panic` and `panic_display` functions accept generic
Expand Down
25 changes: 25 additions & 0 deletions compiler/rustc_const_eval/src/check_consts/ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,31 @@ impl<'tcx> NonConstOp<'tcx> for FnCallIndirect {
}
}

/// A c-variadic function call.
#[derive(Debug)]
pub(crate) struct FnCallCVariadic;
impl<'tcx> NonConstOp<'tcx> for FnCallCVariadic {
fn status_in_item(&self, _ccx: &ConstCx<'_, 'tcx>) -> Status {
Status::Unstable {
gate: sym::const_c_variadic,
gate_already_checked: false,
safe_to_expose_on_stable: false,
is_function_call: true,
}
}

fn build_error(&self, ccx: &ConstCx<'_, 'tcx>, span: Span) -> Diag<'tcx> {
ccx.tcx.sess.create_feature_err(
errors::NonConstCVariadicCall {
span,
kind: ccx.const_kind(),
non_or_conditionally: "non",
},
sym::const_c_variadic,
)
}
}

/// A call to a function that is in a trait, or has trait bounds that make it conditionally-const.
#[derive(Debug)]
pub(crate) struct ConditionallyConstCall<'tcx> {
Expand Down
30 changes: 29 additions & 1 deletion compiler/rustc_const_eval/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,15 @@ pub struct NonConstClosure {
pub non_or_conditionally: &'static str,
}

#[derive(Diagnostic)]
#[diag(const_eval_c_variadic_call, code = E0015)]
pub struct NonConstCVariadicCall {
#[primary_span]
pub span: Span,
pub kind: ConstContext,
pub non_or_conditionally: &'static str,
}

#[derive(Subdiagnostic)]
pub enum NonConstClosureNote {
#[note(const_eval_closure_fndef_not_const)]
Expand Down Expand Up @@ -492,11 +501,13 @@ impl<'a> ReportErrorExt for UndefinedBehaviorInfo<'a> {
WriteToReadOnly(_) => const_eval_write_to_read_only,
DerefFunctionPointer(_) => const_eval_deref_function_pointer,
DerefVTablePointer(_) => const_eval_deref_vtable_pointer,
DerefVaListPointer(_) => const_eval_deref_va_list_pointer,
DerefTypeIdPointer(_) => const_eval_deref_typeid_pointer,
InvalidBool(_) => const_eval_invalid_bool,
InvalidChar(_) => const_eval_invalid_char,
InvalidTag(_) => const_eval_invalid_tag,
InvalidFunctionPointer(_) => const_eval_invalid_function_pointer,
InvalidVaListPointer(_) => const_eval_invalid_va_list_pointer,
InvalidVTablePointer(_) => const_eval_invalid_vtable_pointer,
InvalidVTableTrait { .. } => const_eval_invalid_vtable_trait,
InvalidStr(_) => const_eval_invalid_str,
Expand All @@ -509,8 +520,12 @@ impl<'a> ReportErrorExt for UndefinedBehaviorInfo<'a> {
InvalidNichedEnumVariantWritten { .. } => {
const_eval_invalid_niched_enum_variant_written
}
VaArgOutOfBounds => const_eval_va_arg_out_of_bounds,

AbiMismatchArgument { .. } => const_eval_incompatible_arg_types,
AbiMismatchReturn { .. } => const_eval_incompatible_return_types,
CVariadicMismatch { .. } => const_eval_c_variadic_mismatch,
CVariadicFixedCountMismatch { .. } => const_eval_c_variadic_fixed_count_mismatch,
}
}

Expand All @@ -535,6 +550,7 @@ impl<'a> ReportErrorExt for UndefinedBehaviorInfo<'a> {
| InvalidMeta(InvalidMetaKind::TooBig)
| InvalidUninitBytes(None)
| DeadLocal
| VaArgOutOfBounds
| UninhabitedEnumVariantWritten(_)
| UninhabitedEnumVariantRead(_) => {}

Expand All @@ -555,7 +571,10 @@ impl<'a> ReportErrorExt for UndefinedBehaviorInfo<'a> {
diag.arg("len", len);
diag.arg("index", index);
}
UnterminatedCString(ptr) | InvalidFunctionPointer(ptr) | InvalidVTablePointer(ptr) => {
UnterminatedCString(ptr)
| InvalidFunctionPointer(ptr)
| InvalidVTablePointer(ptr)
| InvalidVaListPointer(ptr) => {
diag.arg("pointer", ptr);
}
InvalidVTableTrait { expected_dyn_type, vtable_dyn_type } => {
Expand Down Expand Up @@ -609,6 +628,7 @@ impl<'a> ReportErrorExt for UndefinedBehaviorInfo<'a> {
WriteToReadOnly(alloc)
| DerefFunctionPointer(alloc)
| DerefVTablePointer(alloc)
| DerefVaListPointer(alloc)
| DerefTypeIdPointer(alloc) => {
diag.arg("allocation", alloc);
}
Expand Down Expand Up @@ -645,6 +665,14 @@ impl<'a> ReportErrorExt for UndefinedBehaviorInfo<'a> {
diag.arg("caller_ty", caller_ty);
diag.arg("callee_ty", callee_ty);
}
CVariadicMismatch { caller_is_c_variadic, callee_is_c_variadic } => {
diag.arg("caller_is_c_variadic", caller_is_c_variadic);
diag.arg("callee_is_c_variadic", callee_is_c_variadic);
}
CVariadicFixedCountMismatch { caller, callee } => {
diag.arg("caller", caller);
diag.arg("callee", callee);
}
}
}
}
Expand Down
80 changes: 70 additions & 10 deletions compiler/rustc_const_eval/src/interpret/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use std::borrow::Cow;
use either::{Left, Right};
use rustc_abi::{self as abi, ExternAbi, FieldIdx, Integer, VariantIdx};
use rustc_data_structures::assert_matches;
use rustc_hir::def::DefKind;
use rustc_hir::def_id::DefId;
use rustc_middle::ty::layout::{IntegerExt, TyAndLayout};
use rustc_middle::ty::{self, AdtDef, Instance, Ty, VariantDef};
Expand All @@ -17,7 +18,7 @@ use tracing::{info, instrument, trace};
use super::{
CtfeProvenance, FnVal, ImmTy, InterpCx, InterpResult, MPlaceTy, Machine, OpTy, PlaceTy,
Projectable, Provenance, ReturnAction, ReturnContinuation, Scalar, StackPopInfo, interp_ok,
throw_ub, throw_ub_custom, throw_unsup_format,
throw_ub, throw_ub_custom,
};
use crate::interpret::EnteredTraceSpan;
use crate::{enter_trace_span, fluent_generated as fluent};
Expand Down Expand Up @@ -349,13 +350,27 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
) -> InterpResult<'tcx> {
let _trace = enter_trace_span!(M, step::init_stack_frame, %instance, tracing_separate_thread = Empty);

// Compute callee information.
// FIXME: for variadic support, do we have to somehow determine callee's extra_args?
let callee_fn_abi = self.fn_abi_of_instance(instance, ty::List::empty())?;
// The check for the DefKind is so that we don't requiest the fn_sig of a closure.
// Otherwise, we hit:
//
// DefId(1:180 ~ std[269c]::rt::lang_start_internal::{closure#0}) does not have a "fn_sig"
let (fixed_count, callee_c_variadic_args) =
if matches!(self.tcx.def_kind(instance.def_id()), DefKind::Fn)
&& let callee_fn_sig = self.tcx.fn_sig(instance.def_id()).skip_binder()
&& callee_fn_sig.c_variadic()
{
// A mismatch in caller and callee fixed_count will error below.
let fixed_count = callee_fn_sig.inputs().skip_binder().len();
let extra_tys = caller_fn_abi.args[caller_fn_abi.fixed_count as usize..]
.iter()
.map(|arg_abi| arg_abi.layout.ty);

(fixed_count, self.tcx.mk_type_list_from_iter(extra_tys))
} else {
(caller_fn_abi.args.len(), ty::List::empty())
};

if callee_fn_abi.c_variadic || caller_fn_abi.c_variadic {
throw_unsup_format!("calling a c-variadic function is not supported");
}
let callee_fn_abi = self.fn_abi_of_instance(instance, callee_c_variadic_args)?;

if caller_fn_abi.conv != callee_fn_abi.conv {
throw_ub_custom!(
Expand All @@ -365,6 +380,20 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
)
}

if caller_fn_abi.c_variadic != callee_fn_abi.c_variadic {
throw_ub!(CVariadicMismatch {
caller_is_c_variadic: caller_fn_abi.c_variadic,
callee_is_c_variadic: callee_fn_abi.c_variadic,
});
}

if caller_fn_abi.fixed_count != callee_fn_abi.fixed_count {
throw_ub!(CVariadicFixedCountMismatch {
caller: caller_fn_abi.fixed_count,
callee: callee_fn_abi.fixed_count,
});
}

// Check that all target features required by the callee (i.e., from
// the attribute `#[target_feature(enable = ...)]`) are enabled at
// compile time.
Expand Down Expand Up @@ -436,16 +465,47 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
// this is a single iterator (that handles `spread_arg`), then
// `pass_argument` would be the loop body. It takes care to
// not advance `caller_iter` for ignored arguments.
let mut callee_args_abis = callee_fn_abi.args.iter().enumerate();
for local in body.args_iter() {
let mut callee_args_abis = if caller_fn_abi.c_variadic {
// Only the fixed arguments are passed normally.
callee_fn_abi.args[..fixed_count].iter().enumerate()
} else {
// NOTE: this handles the extra caller location argument
// when `#[track_caller]` is used.
callee_fn_abi.args.iter().enumerate()
};

let mut it = body.args_iter().peekable();
while let Some(local) = it.next() {
// Construct the destination place for this argument. At this point all
// locals are still dead, so we cannot construct a `PlaceTy`.
let dest = mir::Place::from(local);
// `layout_of_local` does more than just the instantiation we need to get the
// type, but the result gets cached so this avoids calling the instantiation
// query *again* the next time this local is accessed.
let ty = self.layout_of_local(self.frame(), local, None)?.ty;
if Some(local) == body.spread_arg {
if caller_fn_abi.c_variadic && it.peek().is_none() {
Copy link
Member

Choose a reason for hiding this comment

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

So this uses a peekable iterator just so that we can tell if the current argument is the last argument, right? I wonder if there's a more elegant way to do that...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, that's the idea.

idk, I've never found anything obviously better. Manually counting is an option but not great either.

// This is the last callee-side argument of a variadic function.
// This argument is a VaList holding the remaining caller-side arguments.
self.storage_live(local)?;

let place = self.eval_place(dest)?;
let mplace = self.force_allocation(&place)?;

// Consume the remaining arguments by putting them into the variable argument
// list.
let varargs = self.allocate_varargs(&mut caller_args)?;
let key = self.va_list_ptr(varargs);

// Zero the VaList, so it is fully initialized.
self.write_bytes_ptr(
mplace.ptr(),
(0..mplace.layout.size.bytes()).map(|_| 0u8),
)?;

// Store the "key" pointer in the right field.
let key_mplace = self.va_list_key_field(&mplace)?;
self.write_pointer(key, &key_mplace)?;
} else if Some(local) == body.spread_arg {
// Make the local live once, then fill in the value field by field.
self.storage_live(local)?;
// Must be a tuple
Expand Down
Loading