use crate::ast;
use crate::capture::SourceRange;
use crate::ir::{Eightbyte, Expr, ExprKind, Place, RecordId, RustField, Ty};
use crate::target::{Arch, Os};
use super::{Sema, VA_LIST_PLACEMENT};
const MAX_EIGHTBYTES: u64 = 2;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(super) enum VaBuiltin {
Start,
C23Start,
End,
Copy,
}
impl VaBuiltin {
pub(super) fn from_name(name: &str) -> Option<Self> {
Some(match name.strip_prefix("__builtin_")? {
"va_start" => VaBuiltin::Start,
"c23_va_start" => VaBuiltin::C23Start,
"va_end" => VaBuiltin::End,
"va_copy" => VaBuiltin::Copy,
_ => return None,
})
}
fn spelling(self) -> &'static str {
match self {
VaBuiltin::Start | VaBuiltin::C23Start => "va_start",
VaBuiltin::End => "va_end",
VaBuiltin::Copy => "va_copy",
}
}
fn arity_error(self, count: usize) -> Option<String> {
let spelling = self.spelling();
let arity = match self {
VaBuiltin::C23Start if count == 0 => {
return Some(format!("'{spelling}' expects at least 1 argument, have 0"));
}
VaBuiltin::C23Start => return None,
VaBuiltin::End => 1,
VaBuiltin::Start | VaBuiltin::Copy => 2,
};
(count != arity).then(|| format!("'{spelling}' expects {arity} arguments, have {count}"))
}
}
impl Sema<'_> {
pub(super) fn va_builtin(
&mut self,
builtin: VaBuiltin,
args: &[ast::Expr],
range: SourceRange,
) -> Option<Expr> {
if let Some(message) = builtin.arity_error(args.len()) {
self.error(range, message);
return None;
}
match builtin {
VaBuiltin::Start | VaBuiltin::C23Start => {
let ap = self.va_list_lvalue(&args[0])?;
if !self.func_variadic {
self.error(range, "'va_start' used in a function with fixed arguments");
return None;
}
if builtin == VaBuiltin::Start {
self.check_va_start_parameter(&args[1]);
}
let value = Expr::new(ExprKind::VaListPristine, Ty::VaList, range);
Some(assign(ap, value, range))
}
VaBuiltin::End => {
self.va_list_lvalue(&args[0])?;
Some(Expr::new(ExprKind::VaEnd, Ty::Void, range))
}
VaBuiltin::Copy => {
let dst = self.va_list_lvalue(&args[0])?;
let src = self.expr(&args[1])?;
if src.ty.is_error() {
return None;
}
if !src.ty.is_va_list() {
self.error(
args[1].range,
format!(
"the second argument of 'va_copy' must have type 'va_list', not '{}'",
self.tyname(src.ty)
),
);
return None;
}
Some(assign(dst, src, range))
}
}
}
pub(super) fn va_arg(
&mut self,
ap: &ast::Expr,
type_name: &ast::TypeName,
range: SourceRange,
) -> Option<Expr> {
let place = self.va_list_lvalue(ap)?;
let target = self.ty_of(&type_name.ty)?;
let record = self.check_va_arg_type(target, type_name.range)?;
Some(Expr::new(
ExprKind::VaArg { ap: place, record },
target,
range,
))
}
#[expect(clippy::option_option, reason = "the outer one is the error")]
fn check_va_arg_type(&mut self, ty: Ty, range: SourceRange) -> Option<Option<Vec<Eightbyte>>> {
if ty.is_error() {
return None;
}
if ty.is_pointer() || ty.is_enum() || ty == Ty::Double {
return Some(None);
}
if ty.is_int128() {
self.error(
range,
format!(
"va_arg with '{}' is not supported: Rust's `VaArgSafe` covers the \
128-bit types only behind the unstable `c_variadic_int128` feature",
self.tyname(ty)
),
);
return None;
}
if ty.is_record() || ty.is_complex() {
return self.record_eightbytes(ty, range).map(Some);
}
if ty.is_arithmetic() {
let promoted = ty.promote_argument(&self.target);
if promoted == ty {
return Some(None);
}
self.error(
range,
format!(
"'{}' is promoted to '{}' when passed through '...'; you should pass \
'{}' not '{}' to 'va_arg'",
self.tyname(ty),
self.tyname(promoted),
self.tyname(promoted),
self.tyname(ty)
),
);
return None;
}
self.error(
range,
format!("va_arg with type '{}' is not supported", self.tyname(ty)),
);
None
}
fn record_eightbytes(&mut self, ty: Ty, range: SourceRange) -> Option<Vec<Eightbyte>> {
let refuse = |sema: &mut Self, why: &str| {
sema.error(
range,
format!("va_arg with '{}' is not supported: {why}", sema.tyname(ty)),
);
None::<Vec<Eightbyte>>
};
if self.target.arch != Arch::X86_64 || self.target.os == Os::Windows {
if ty.is_complex() {
return refuse(
self,
"a complex value is a pair, so it is read back the way an aggregate is, \
which is only supported on x86-64 System V targets",
);
}
self.error(
range,
"va_arg of a struct type is only supported on x86-64 System V targets",
);
return None;
}
if !self.types().is_complete(ty) {
return refuse(self, "the type is incomplete");
}
let Some(layout) = self.types().size_align(ty, &self.target) else {
return refuse(self, "the type has no size");
};
let eightbytes = layout.size.div_ceil(8);
if eightbytes > MAX_EIGHTBYTES {
return refuse(
self,
&format!(
"it is {} bytes, and the x86-64 System V ABI passes a struct larger \
than 16 bytes on the stack, where Rust's 'va_list' cannot reach it",
layout.size
),
);
}
let mut classes = vec![Eightbyte::None; eightbytes as usize];
if self.classify(ty, 0, &mut classes).is_none() {
return refuse(
self,
"a member is not aligned the way its own type asks, so the x86-64 System V \
ABI passes the struct on the stack, where Rust's 'va_list' cannot reach it",
);
}
Some(classes)
}
fn classify(&self, ty: Ty, offset: u64, classes: &mut [Eightbyte]) -> Option<()> {
let ty = self.types().unatomic(ty);
let layout = self.types().size_align(ty, &self.target)?;
if layout.size == 0 {
return Some(());
}
if !offset.is_multiple_of(layout.align.max(1)) {
return None;
}
let class = match ty {
Ty::Float | Ty::Double => Eightbyte::Sse,
Ty::ComplexFloat | Ty::ComplexDouble => {
let half = ty.complex_component();
self.classify(half, offset, classes)?;
let stride = half.size_bytes(&self.target);
return self.classify(half, offset + stride, classes);
}
Ty::Array(id) => {
let elem = self.types().array_type(id).elem;
let stride = self.types().size_of(elem, &self.target)?.max(1);
let mut at = offset;
while at < offset + layout.size {
self.classify(elem, at, classes)?;
at += stride;
}
return Some(());
}
Ty::Record(id) => return self.classify_record(id, offset, classes),
_ => Eightbyte::Int,
};
merge(classes, offset, layout.size, class);
Some(())
}
fn classify_record(&self, id: RecordId, offset: u64, classes: &mut [Eightbyte]) -> Option<()> {
let record = self.types().record(id);
for field in &record.rust_fields {
match field {
RustField::Member(index) => {
let member = &record.fields[*index];
if member.flexible {
continue;
}
self.classify(member.ty, offset + member.offset, classes)?;
}
RustField::Bits {
offset: at, bytes, ..
} => merge(classes, offset + at, *bytes, Eightbyte::Int),
RustField::Pad { .. } | RustField::Align { .. } => {}
}
}
Some(())
}
fn va_list_lvalue(&mut self, expr: &ast::Expr) -> Option<Place> {
let place = self.lvalue_assignable(expr)?;
if place.ty.is_error() {
return None;
}
if !place.ty.is_va_list() {
self.error(
expr.range,
format!(
"expected an object of type 'va_list', not '{}'",
self.tyname(place.ty)
),
);
return None;
}
Some(place)
}
fn check_va_start_parameter(&mut self, arg: &ast::Expr) {
if let ast::ExprKind::Ident(name) = &arg.kind
&& let Some(super::Entry::Object(id)) = self.lookup(&name.name)
&& self.func_params.contains(id)
{
return;
}
let func = self.func_name.clone();
self.error(
arg.range,
format!("the second argument of 'va_start' must name a parameter of '{func}'"),
);
}
pub(super) fn va_list_init(&mut self, range: SourceRange) -> Option<Expr> {
if self.func_variadic || self.va_param.is_some() {
return Some(Expr::new(ExprKind::VaListPristine, Ty::VaList, range));
}
self.error(
range,
"a 'va_list' variable can only be declared in a variadic function or in one \
that takes a 'va_list' parameter",
);
None
}
pub(super) fn reject_va_list(&mut self, ty: Ty, range: SourceRange) -> bool {
if !self.mentions_va_list(ty) {
return false;
}
self.error(range, VA_LIST_PLACEMENT);
true
}
pub(super) fn points_to_va_list(&self, ty: Ty) -> bool {
self.types()
.pointee(ty)
.is_some_and(|pointee| pointee.is_va_list())
}
pub(super) fn mentions_va_list(&self, ty: Ty) -> bool {
match ty {
Ty::VaList => true,
Ty::Pointer(id) => self.mentions_va_list(self.types().pointer_type(id).pointee),
Ty::Array(id) => self.mentions_va_list(self.types().array_type(id).elem),
_ => false,
}
}
}
fn merge(classes: &mut [Eightbyte], offset: u64, size: u64, class: Eightbyte) {
if size == 0 {
return;
}
let first = (offset / 8) as usize;
let last = ((offset + size - 1) / 8) as usize;
for slot in classes.iter_mut().take(last + 1).skip(first) {
*slot = match *slot {
Eightbyte::None => class,
Eightbyte::Int => Eightbyte::Int,
Eightbyte::Sse => class,
};
}
}
fn assign(place: Place, value: Expr, range: SourceRange) -> Expr {
Expr::new(
ExprKind::Assign {
place,
value: Box::new(value),
},
Ty::Void,
range,
)
}