use crate::ast;
use crate::capture::SourceRange;
use crate::ir::{
self, AtomicClass, AtomicExpr, AtomicOp, AtomicRmw, BinOp, Expr, ExprKind, MemOrder, PlaceKind,
Ty,
};
use super::{Sema, place_of};
#[derive(Clone, Copy, PartialEq, Eq)]
enum Family {
Atomic,
Sync,
C11,
}
pub const ATOMIC_BUILTINS: &[&str] = &[
"add_fetch",
"always_lock_free",
"and_fetch",
"clear",
"compare_exchange",
"compare_exchange_n",
"exchange",
"exchange_n",
"fetch_add",
"fetch_and",
"fetch_nand",
"fetch_or",
"fetch_sub",
"fetch_xor",
"is_lock_free",
"load",
"load_n",
"nand_fetch",
"or_fetch",
"signal_fence",
"store",
"store_n",
"sub_fetch",
"test_and_set",
"thread_fence",
"xor_fetch",
];
pub const SYNC_BUILTINS: &[&str] = &[
"add_and_fetch",
"and_and_fetch",
"bool_compare_and_swap",
"fetch_and_add",
"fetch_and_and",
"fetch_and_nand",
"fetch_and_or",
"fetch_and_sub",
"fetch_and_xor",
"lock_release",
"lock_test_and_set",
"nand_and_fetch",
"or_and_fetch",
"sub_and_fetch",
"synchronize",
"val_compare_and_swap",
"xor_and_fetch",
];
pub const C11_ATOMIC_BUILTINS: &[&str] = &[
"compare_exchange_strong",
"compare_exchange_weak",
"exchange",
"fetch_add",
"fetch_and",
"fetch_nand",
"fetch_or",
"fetch_sub",
"fetch_xor",
"init",
"is_lock_free",
"load",
"signal_fence",
"store",
"thread_fence",
];
fn family_of(name: &str) -> Option<(Family, &str)> {
let (family, rest, table) = if let Some(rest) = name.strip_prefix("__c11_atomic_") {
(Family::C11, rest, C11_ATOMIC_BUILTINS)
} else if let Some(rest) = name.strip_prefix("__atomic_") {
(Family::Atomic, rest, ATOMIC_BUILTINS)
} else {
let rest = name.strip_prefix("__sync_")?;
(Family::Sync, rest, SYNC_BUILTINS)
};
table.contains(&rest).then_some((family, rest))
}
pub fn is_atomic_builtin(name: &str) -> bool {
family_of(name).is_some()
}
fn order_of(value: i128) -> Option<MemOrder> {
Some(match value {
0 => MemOrder::Relaxed,
1 | 2 => MemOrder::Acquire,
3 => MemOrder::Release,
4 => MemOrder::AcqRel,
5 => MemOrder::SeqCst,
_ => return None,
})
}
fn rmw_of(word: &str) -> Option<AtomicRmw> {
Some(match word {
"add" => AtomicRmw::Add,
"sub" => AtomicRmw::Sub,
"and" => AtomicRmw::And,
"or" => AtomicRmw::Or,
"xor" => AtomicRmw::Xor,
"nand" => AtomicRmw::Nand,
_ => return None,
})
}
struct Call<'a> {
name: &'a str,
family: Family,
range: SourceRange,
}
impl Sema<'_> {
pub(super) fn atomic_builtin(
&mut self,
name: &str,
args: &[ast::Expr],
range: SourceRange,
) -> Option<Option<Expr>> {
let (family, rest) = family_of(name)?;
let call = Call {
name,
family,
range,
};
let result = match family {
Family::Atomic => self.gcc_atomic(&call, rest, args),
Family::Sync => self.gcc_sync(&call, rest, args),
Family::C11 => self.c11_atomic(&call, rest, args),
};
let pending = std::mem::take(&mut self.pending_discard);
Some(result.map(|expr| self.with_discarded(expr, pending)))
}
fn with_discarded(&mut self, expr: Expr, discarded: Vec<Expr>) -> Expr {
let mut out = expr;
for value in discarded.into_iter().rev() {
let (ty, range) = (out.ty, out.range);
out = Expr::new(
ExprKind::Comma {
lhs: Box::new(value),
rhs: Box::new(out),
},
ty,
range,
);
}
out
}
fn gcc_atomic(&mut self, call: &Call, rest: &str, args: &[ast::Expr]) -> Option<Expr> {
match rest {
"thread_fence" | "signal_fence" => {
self.arity(call, args, 1)?;
let order = self.order_arg(call, &args[0], MemOrder::SeqCst);
self.fence(call, rest == "signal_fence", order)
}
"always_lock_free" | "is_lock_free" => self.lock_free(call, args, 2),
"load_n" | "load" => {
let generic = rest == "load";
self.arity(call, args, if generic { 3 } else { 2 })?;
let object = self.atomic_object(call, &args[0])?;
let order = self.order_arg(call, &args[args.len() - 1], MemOrder::SeqCst);
self.check_load_order(call, order, args[args.len() - 1].range)?;
let load = self.atomic_node(AtomicOp::Load, &object, None, None, order, order);
if !generic {
return Some(load);
}
self.store_through(call, &args[1], load)
}
"store_n" | "store" => {
let generic = rest == "store";
self.arity(call, args, 3)?;
let object = self.atomic_object(call, &args[0])?;
let value = self.value_arg(call, &args[1], &object, generic)?;
let order = self.order_arg(call, &args[2], MemOrder::SeqCst);
self.check_store_order(call, order, args[2].range)?;
Some(self.atomic_node(AtomicOp::Store, &object, Some(value), None, order, order))
}
"exchange_n" | "exchange" => {
let generic = rest == "exchange";
self.arity(call, args, if generic { 4 } else { 3 })?;
let object = self.atomic_object(call, &args[0])?;
let value = self.value_arg(call, &args[1], &object, generic)?;
let order = self.order_arg(call, &args[args.len() - 1], MemOrder::SeqCst);
let swap =
self.atomic_node(AtomicOp::Exchange, &object, Some(value), None, order, order);
if !generic {
return Some(swap);
}
self.store_through(call, &args[2], swap)
}
"compare_exchange_n" | "compare_exchange" => {
let generic = rest == "compare_exchange";
self.arity(call, args, 6)?;
let object = self.atomic_object(call, &args[0])?;
let expected = self.expected_arg(call, &args[1], &object)?;
let desired = self.value_arg(call, &args[2], &object, generic)?;
let weak = self.flag_arg(&args[3]);
let (success, failure) = self.cas_orders(call, &args[4], &args[5])?;
Some(self.atomic_node(
AtomicOp::CompareExchange { weak },
&object,
Some(desired),
Some(expected),
success,
failure,
))
}
"test_and_set" | "clear" => {
self.arity(call, args, 2)?;
let object = self.byte_object(call, &args[0])?;
let order = self.order_arg(call, &args[1], MemOrder::SeqCst);
if rest == "clear" {
self.check_store_order(call, order, args[1].range)?;
return Some(self.atomic_node(
AtomicOp::Clear,
&object,
None,
None,
order,
order,
));
}
Some(self.atomic_node(AtomicOp::TestAndSet, &object, None, None, order, order))
}
_ => {
let (word, returns_new) = match rest.strip_prefix("fetch_") {
Some(word) => (word, false),
None => (rest.strip_suffix("_fetch")?, true),
};
let op = rmw_of(word)?;
self.arity(call, args, 3)?;
let object = self.atomic_object(call, &args[0])?;
let value = self.rmw_operand(call, &args[1], &object, op, false)?;
let order = self.order_arg(call, &args[2], MemOrder::SeqCst);
Some(self.atomic_node(
AtomicOp::Rmw { op, returns_new },
&object,
Some(value),
None,
order,
order,
))
}
}
}
fn gcc_sync(&mut self, call: &Call, rest: &str, args: &[ast::Expr]) -> Option<Expr> {
let seq = MemOrder::SeqCst;
match rest {
"synchronize" => {
self.discard_rest(args, 0);
self.fence(call, false, seq)
}
"lock_release" => {
self.at_least(call, args, 1)?;
let object = self.atomic_object(call, &args[0])?;
self.discard_rest(args, 1);
let value = self.zero_operand(&object, call.range);
Some(self.atomic_node(
AtomicOp::Store,
&object,
Some(value),
None,
MemOrder::Release,
MemOrder::Release,
))
}
"lock_test_and_set" => {
self.at_least(call, args, 2)?;
let object = self.atomic_object(call, &args[0])?;
let value = self.value_arg(call, &args[1], &object, false)?;
self.discard_rest(args, 2);
Some(self.atomic_node(
AtomicOp::Exchange,
&object,
Some(value),
None,
MemOrder::Acquire,
MemOrder::Acquire,
))
}
"bool_compare_and_swap" | "val_compare_and_swap" => {
self.at_least(call, args, 3)?;
let object = self.atomic_object(call, &args[0])?;
let old = self.value_arg(call, &args[1], &object, false)?;
let new = self.value_arg(call, &args[2], &object, false)?;
self.discard_rest(args, 3);
let value_is_old = rest.starts_with("val");
Some(self.atomic_node(
AtomicOp::SyncCompareSwap { value_is_old },
&object,
Some(new),
Some(old),
seq,
seq,
))
}
_ => {
let (word, returns_new) = match rest.strip_prefix("fetch_and_") {
Some(word) => (word, false),
None => (rest.strip_suffix("_and_fetch")?, true),
};
let op = rmw_of(word)?;
self.at_least(call, args, 2)?;
let object = self.atomic_object(call, &args[0])?;
let value = self.rmw_operand(call, &args[1], &object, op, false)?;
self.discard_rest(args, 2);
Some(self.atomic_node(
AtomicOp::Rmw { op, returns_new },
&object,
Some(value),
None,
seq,
seq,
))
}
}
}
fn c11_atomic(&mut self, call: &Call, rest: &str, args: &[ast::Expr]) -> Option<Expr> {
match rest {
"thread_fence" | "signal_fence" => {
self.arity(call, args, 1)?;
let order = self.order_arg(call, &args[0], MemOrder::SeqCst);
self.fence(call, rest == "signal_fence", order)
}
"is_lock_free" => self.lock_free(call, args, 1),
"init" => {
self.arity(call, args, 2)?;
let object = self.atomic_object(call, &args[0])?;
let value = self.value_arg(call, &args[1], &object, false)?;
let place = place_of(
PlaceKind::Deref(Box::new(object.ptr.clone())),
object.value_ty,
false,
call.range,
);
Some(Expr::new(
ExprKind::Assign {
place,
value: Box::new(value),
},
object.value_ty,
call.range,
))
}
"load" => {
self.arity(call, args, 2)?;
let object = self.atomic_object(call, &args[0])?;
let order = self.order_arg(call, &args[1], MemOrder::SeqCst);
self.check_load_order(call, order, args[1].range)?;
Some(self.atomic_node(AtomicOp::Load, &object, None, None, order, order))
}
"store" => {
self.arity(call, args, 3)?;
let object = self.atomic_object(call, &args[0])?;
let value = self.value_arg(call, &args[1], &object, false)?;
let order = self.order_arg(call, &args[2], MemOrder::SeqCst);
self.check_store_order(call, order, args[2].range)?;
Some(self.atomic_node(AtomicOp::Store, &object, Some(value), None, order, order))
}
"exchange" => {
self.arity(call, args, 3)?;
let object = self.atomic_object(call, &args[0])?;
let value = self.value_arg(call, &args[1], &object, false)?;
let order = self.order_arg(call, &args[2], MemOrder::SeqCst);
Some(self.atomic_node(AtomicOp::Exchange, &object, Some(value), None, order, order))
}
"compare_exchange_strong" | "compare_exchange_weak" => {
self.arity(call, args, 5)?;
let object = self.atomic_object(call, &args[0])?;
let expected = self.expected_arg(call, &args[1], &object)?;
let desired = self.value_arg(call, &args[2], &object, false)?;
let (success, failure) = self.cas_orders(call, &args[3], &args[4])?;
let weak = rest.ends_with("weak");
Some(self.atomic_node(
AtomicOp::CompareExchange { weak },
&object,
Some(desired),
Some(expected),
success,
failure,
))
}
_ => {
let word = rest.strip_prefix("fetch_")?;
let op = rmw_of(word)?;
self.arity(call, args, 3)?;
let object = self.atomic_object(call, &args[0])?;
let value = self.rmw_operand(call, &args[1], &object, op, true)?;
let order = self.order_arg(call, &args[2], MemOrder::SeqCst);
Some(self.atomic_node(
AtomicOp::Rmw {
op,
returns_new: false,
},
&object,
Some(value),
None,
order,
order,
))
}
}
}
fn atomic_node(
&mut self,
op: AtomicOp,
object: &AtomicObject,
value: Option<Expr>,
expected: Option<Expr>,
success: MemOrder,
failure: MemOrder,
) -> Expr {
let ty = match op {
AtomicOp::Load | AtomicOp::Exchange => object.value_ty,
AtomicOp::Rmw { .. } => object.value_ty,
AtomicOp::SyncCompareSwap { value_is_old: true } => object.value_ty,
AtomicOp::SyncCompareSwap { .. } | AtomicOp::CompareExchange { .. } => Ty::Bool,
AtomicOp::TestAndSet => Ty::Bool,
AtomicOp::Store | AtomicOp::Clear | AtomicOp::Fence { .. } => Ty::Void,
};
let range = object.range;
Expr::new(
ExprKind::Atomic(Box::new(AtomicExpr {
op,
class: object.class,
value_ty: object.value_ty,
ptr: Some(object.ptr.clone()),
value,
expected,
success,
failure,
})),
ty,
range,
)
}
fn fence(&mut self, call: &Call, signal: bool, order: MemOrder) -> Option<Expr> {
Some(Expr::new(
ExprKind::Atomic(Box::new(AtomicExpr {
op: AtomicOp::Fence { signal },
class: AtomicClass::Int {
bytes: 4,
signed: true,
},
value_ty: Ty::Void,
ptr: None,
value: None,
expected: None,
success: order,
failure: order,
})),
Ty::Void,
call.range,
))
}
fn lock_free(&mut self, call: &Call, args: &[ast::Expr], arity: usize) -> Option<Expr> {
self.arity(call, args, arity)?;
let size = self.expr(&args[0])?;
for arg in &args[1..] {
self.expr(arg);
}
let answer = match self.const_eval(&size) {
Some(ir::ConstValue::Int(bytes)) => {
i128::from(matches!(bytes, 1 | 2 | 4 | 8) && bytes <= self.max_atomic_bytes())
}
_ => 0,
};
Some(Expr::int(answer, Ty::Bool, call.range))
}
fn max_atomic_bytes(&self) -> i128 {
i128::from(self.target.max_scalar_align).min(8)
}
fn atomic_object(&mut self, call: &Call, arg: &ast::Expr) -> Option<AtomicObject> {
let ptr = self.expr(arg)?;
let Some(pointee) = self.pointee(ptr.ty) else {
self.error(
arg.range,
format!(
"the first argument of '{}' must be a pointer, not '{}'",
call.name,
self.tyname(ptr.ty)
),
);
return None;
};
if call.family == Family::C11 && !pointee.is_atomic() {
self.error(
arg.range,
format!(
"the first argument of '{}' must be a pointer to an '_Atomic' type, \
and '{}' is not",
call.name,
self.tyname(ptr.ty)
),
);
return None;
}
let value_ty = self.types().unatomic(pointee);
let class = self.atomic_class_of(call, value_ty, arg.range)?;
Some(AtomicObject {
ptr,
value_ty,
class,
range: call.range,
})
}
fn byte_object(&mut self, call: &Call, arg: &ast::Expr) -> Option<AtomicObject> {
let ptr = self.expr(arg)?;
let Some(pointee) = self.pointee(ptr.ty) else {
self.error(
arg.range,
format!(
"the first argument of '{}' must be a pointer, not '{}'",
call.name,
self.tyname(ptr.ty)
),
);
return None;
};
let value_ty = self.types().unatomic(pointee);
if self.size_of(value_ty) != Some(1) || !value_ty.is_scalar() {
self.error(
arg.range,
format!(
"'{}' needs a pointer to a one-byte object, and '{}' points at '{}'",
call.name,
self.tyname(ptr.ty),
self.tyname(value_ty)
),
);
return None;
}
Some(AtomicObject {
ptr,
value_ty,
class: AtomicClass::Int {
bytes: 1,
signed: false,
},
range: call.range,
})
}
fn atomic_class_of(&mut self, call: &Call, ty: Ty, range: SourceRange) -> Option<AtomicClass> {
if let Some(class) = ir::atomic_class(self.types(), ty, &self.target) {
if let Some(bytes) = self.size_of(ty)
&& i128::from(bytes) > self.max_atomic_bytes()
{
self.error(
range,
format!(
"'{}' on '{}' is not supported on this target: the object is {bytes} \
bytes and this ABI aligns it to {}, which a lock-free atomic of that \
width cannot be built on",
call.name,
self.tyname(ty),
self.target.max_scalar_align
),
);
return None;
}
return Some(class);
}
let reason = if ty.is_int128() {
"there is no stable 128-bit atomic in `core::sync::atomic`"
} else if ty.is_record() || ty.is_array() {
"only the scalar types have a lock-free atomic in `core::sync::atomic`"
} else {
"there is no atomic of that type in `core::sync::atomic`"
};
self.error(
range,
format!(
"'{}' cannot operate on '{}': {reason}",
call.name,
self.tyname(ty)
),
);
None
}
fn value_arg(
&mut self,
call: &Call,
arg: &ast::Expr,
object: &AtomicObject,
indirect: bool,
) -> Option<Expr> {
let value = self.expr(arg)?;
if !indirect {
return Some(self.convert(value, object.value_ty));
}
let Some(pointee) = self.pointee(value.ty) else {
self.error(
arg.range,
format!(
"'{}' takes a pointer to the value, and '{}' is not one",
call.name,
self.tyname(value.ty)
),
);
return None;
};
let pointee = self.types().unatomic(pointee);
let place = place_of(PlaceKind::Deref(Box::new(value)), pointee, false, arg.range);
let loaded = Expr::new(ExprKind::Load(place), pointee, arg.range);
Some(self.convert(loaded, object.value_ty))
}
fn expected_arg(
&mut self,
call: &Call,
arg: &ast::Expr,
object: &AtomicObject,
) -> Option<Expr> {
let ptr = self.expr(arg)?;
let Some(pointee) = self.pointee(ptr.ty) else {
self.error(
arg.range,
format!(
"the 'expected' argument of '{}' must be a pointer, not '{}'",
call.name,
self.tyname(ptr.ty)
),
);
return None;
};
if self.types().unatomic(pointee) != object.value_ty {
self.error(
arg.range,
format!(
"the 'expected' argument of '{}' must point at '{}', not at '{}'",
call.name,
self.tyname(object.value_ty),
self.tyname(pointee)
),
);
return None;
}
Some(ptr)
}
fn rmw_operand(
&mut self,
call: &Call,
arg: &ast::Expr,
object: &AtomicObject,
op: AtomicRmw,
scaled: bool,
) -> Option<Expr> {
let value = self.expr(arg)?;
match object.class {
AtomicClass::Float { .. } => {
self.error(
call.range,
format!(
"'{}' does not work on '{}': the atomic arithmetic builtins take an \
integer or a pointer object",
call.name,
self.tyname(object.value_ty)
),
);
None
}
AtomicClass::FnPtr => {
self.error(
call.range,
format!(
"'{}' does not work on '{}': there is no arithmetic on a function \
pointer",
call.name,
self.tyname(object.value_ty)
),
);
None
}
AtomicClass::Ptr => {
if !matches!(op, AtomicRmw::Add | AtomicRmw::Sub) {
self.error(
call.range,
format!(
"'{}' does not work on a pointer object: only '+' and '-' do",
call.name
),
);
return None;
}
if !value.ty.is_integer() {
self.error(
arg.range,
format!(
"'{}' on a pointer object takes an integer operand, not '{}'",
call.name,
self.tyname(value.ty)
),
);
return None;
}
let diff = Ty::ptrdiff_ty(&self.target);
let value = self.convert(value, diff);
if !scaled {
return Some(value);
}
let pointee = self.pointee(object.value_ty)?;
let size = self.size_of(pointee).unwrap_or(1).max(1);
if size == 1 {
return Some(value);
}
let scale = Expr::int(i128::from(size), diff, arg.range);
Some(Expr::new(
ExprKind::Binary {
op: BinOp::Mul,
lhs: Box::new(value),
rhs: Box::new(scale),
},
diff,
arg.range,
))
}
AtomicClass::Bool => {
if matches!(op, AtomicRmw::Add | AtomicRmw::Sub) {
self.error(
call.range,
format!(
"'{}' does not work on a '_Bool' object: adding to one has no \
meaning",
call.name
),
);
return None;
}
Some(self.convert(value, Ty::Bool))
}
AtomicClass::Int { .. } => {
if !value.ty.is_integer() {
self.error(
arg.range,
format!(
"'{}' takes an integer operand, not '{}'",
call.name,
self.tyname(value.ty)
),
);
return None;
}
Some(self.convert(value, object.value_ty))
}
}
}
fn zero_operand(&mut self, object: &AtomicObject, range: SourceRange) -> Expr {
Expr::new(ExprKind::Zeroed, object.value_ty, range)
}
fn order_arg(&mut self, call: &Call, arg: &ast::Expr, fallback: MemOrder) -> MemOrder {
let Some(value) = self.expr(arg) else {
return fallback;
};
let Some(ir::ConstValue::Int(order)) = self.const_eval(&value) else {
self.pending_discard.push(value);
return fallback;
};
match order_of(order) {
Some(order) => order,
None => {
self.error(
arg.range,
format!(
"'{}' has no memory order {order}; write one of the '__ATOMIC_…' \
macros or a 'memory_order_…' constant",
call.name
),
);
fallback
}
}
}
fn cas_orders(
&mut self,
call: &Call,
success: &ast::Expr,
failure: &ast::Expr,
) -> Option<(MemOrder, MemOrder)> {
let ok = self.order_arg(call, success, MemOrder::SeqCst);
let bad = self.order_arg(call, failure, MemOrder::SeqCst);
if !bad.valid_for_load() {
self.error(
failure.range,
format!(
"the failure memory order of '{}' may not be '{}': it describes a load",
call.name,
bad.c_name()
),
);
return None;
}
if bad.strength() > ok.strength() {
self.error(
failure.range,
format!(
"the failure memory order of '{}' may not be stronger than the success \
order ('{}' against '{}')",
call.name,
bad.c_name(),
ok.c_name()
),
);
return None;
}
Some((ok, bad))
}
fn check_load_order(&mut self, call: &Call, order: MemOrder, range: SourceRange) -> Option<()> {
if order.valid_for_load() {
return Some(());
}
self.error(
range,
format!(
"'{}' may not be performed with '{}': it is a load, and a load has nothing \
to release",
call.name,
order.c_name()
),
);
None
}
fn check_store_order(
&mut self,
call: &Call,
order: MemOrder,
range: SourceRange,
) -> Option<()> {
if order.valid_for_store() {
return Some(());
}
self.error(
range,
format!(
"'{}' may not be performed with '{}': it is a store, and a store has nothing \
to acquire",
call.name,
order.c_name()
),
);
None
}
fn flag_arg(&mut self, arg: &ast::Expr) -> bool {
let Some(value) = self.expr(arg) else {
return false;
};
match self.const_eval(&value) {
Some(ir::ConstValue::Int(flag)) => flag != 0,
_ => {
self.pending_discard.push(value);
false
}
}
}
fn store_through(&mut self, call: &Call, arg: &ast::Expr, value: Expr) -> Option<Expr> {
let ptr = self.expr(arg)?;
let Some(pointee) = self.pointee(ptr.ty) else {
self.error(
arg.range,
format!(
"'{}' writes its result through a pointer, and '{}' is not one",
call.name,
self.tyname(ptr.ty)
),
);
return None;
};
let pointee = self.types().unatomic(pointee);
let place = place_of(PlaceKind::Deref(Box::new(ptr)), pointee, false, arg.range);
let value = self.convert(value, pointee);
Some(Expr::new(
ExprKind::Assign {
place,
value: Box::new(value),
},
pointee,
call.range,
))
}
fn discard_rest(&mut self, args: &[ast::Expr], from: usize) {
for arg in args.iter().skip(from) {
if let Some(value) = self.expr(arg) {
self.pending_discard.push(value);
}
}
}
fn arity(&mut self, call: &Call, args: &[ast::Expr], wanted: usize) -> Option<()> {
if args.len() == wanted {
return Some(());
}
self.error(
call.range,
format!(
"'{}' expects {wanted} argument{}, have {}",
call.name,
if wanted == 1 { "" } else { "s" },
args.len()
),
);
None
}
fn at_least(&mut self, call: &Call, args: &[ast::Expr], wanted: usize) -> Option<()> {
if args.len() >= wanted {
return Some(());
}
self.error(
call.range,
format!(
"'{}' expects at least {wanted} argument{}, have {}",
call.name,
if wanted == 1 { "" } else { "s" },
args.len()
),
);
None
}
}
struct AtomicObject {
ptr: Expr,
value_ty: Ty,
class: AtomicClass,
range: SourceRange,
}