use std::fmt;
use idakit_sys as sys;
use crate::error::Result;
use crate::ffi::nul_checked;
use crate::function::CallingConvention;
#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)]
pub enum TypeExpr {
Void,
Bool,
Int {
bytes: u8,
signed: bool,
},
Float {
bytes: u8,
},
Bitfield {
nbytes: u8,
width: u8,
signed: bool,
},
Decl(String),
Named(String),
Pointer(Box<Self>),
Array {
elem: Box<Self>,
len: u64,
},
Const(Box<Self>),
Volatile(Box<Self>),
Function {
ret: Box<Self>,
params: Vec<Param>,
varargs: bool,
cc: Option<CallingConvention>,
},
}
#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)]
pub struct Param {
pub name: Option<String>,
pub ty: TypeExpr,
}
#[must_use]
pub fn named(name: impl Into<String>) -> TypeExpr {
TypeExpr::Named(name.into())
}
#[must_use]
pub fn decl(text: impl Into<String>) -> TypeExpr {
TypeExpr::Decl(text.into())
}
#[must_use]
pub fn function(ret: impl Into<TypeExpr>) -> FunctionExpr {
FunctionExpr {
ret: Box::new(ret.into()),
params: Vec::new(),
varargs: false,
cc: None,
}
}
#[derive(Clone, Debug)]
pub struct FunctionExpr {
ret: Box<TypeExpr>,
params: Vec<Param>,
varargs: bool,
cc: Option<CallingConvention>,
}
impl FunctionExpr {
#[must_use]
pub fn arg(mut self, ty: impl Into<TypeExpr>) -> Self {
self.params.push(Param {
name: None,
ty: ty.into(),
});
self
}
#[must_use]
pub fn named_arg(mut self, name: impl Into<String>, ty: impl Into<TypeExpr>) -> Self {
self.params.push(Param {
name: Some(name.into()),
ty: ty.into(),
});
self
}
#[must_use]
pub fn variadic(mut self) -> Self {
self.varargs = true;
self
}
#[must_use]
pub fn calling_convention(mut self, cc: CallingConvention) -> Self {
self.cc = Some(cc);
self
}
#[must_use]
pub fn build(self) -> TypeExpr {
TypeExpr::Function {
ret: self.ret,
params: self.params,
varargs: self.varargs,
cc: self.cc,
}
}
}
impl From<FunctionExpr> for TypeExpr {
#[inline]
fn from(builder: FunctionExpr) -> Self {
builder.build()
}
}
#[must_use]
pub fn void() -> TypeExpr {
TypeExpr::Void
}
#[must_use]
pub fn bool_() -> TypeExpr {
TypeExpr::Bool
}
#[must_use]
pub fn char_() -> TypeExpr {
TypeExpr::Int {
bytes: 1,
signed: true,
}
}
#[must_use]
pub fn int8() -> TypeExpr {
TypeExpr::Int {
bytes: 1,
signed: true,
}
}
#[must_use]
pub fn int16() -> TypeExpr {
TypeExpr::Int {
bytes: 2,
signed: true,
}
}
#[must_use]
pub fn int32() -> TypeExpr {
TypeExpr::Int {
bytes: 4,
signed: true,
}
}
#[must_use]
pub fn int64() -> TypeExpr {
TypeExpr::Int {
bytes: 8,
signed: true,
}
}
#[must_use]
pub fn uint8() -> TypeExpr {
TypeExpr::Int {
bytes: 1,
signed: false,
}
}
#[must_use]
pub fn uint16() -> TypeExpr {
TypeExpr::Int {
bytes: 2,
signed: false,
}
}
#[must_use]
pub fn uint32() -> TypeExpr {
TypeExpr::Int {
bytes: 4,
signed: false,
}
}
#[must_use]
pub fn uint64() -> TypeExpr {
TypeExpr::Int {
bytes: 8,
signed: false,
}
}
#[must_use]
pub fn float32() -> TypeExpr {
TypeExpr::Float { bytes: 4 }
}
#[must_use]
pub fn float64() -> TypeExpr {
TypeExpr::Float { bytes: 8 }
}
#[must_use]
pub fn bitfield(nbytes: u8, width: u8, signed: bool) -> TypeExpr {
TypeExpr::Bitfield {
nbytes,
width,
signed,
}
}
impl TypeExpr {
#[inline]
#[must_use]
pub fn pointer(self) -> Self {
Self::Pointer(Box::new(self))
}
#[inline]
#[must_use]
pub fn array(self, len: u64) -> Self {
Self::Array {
elem: Box::new(self),
len,
}
}
#[inline]
#[must_use]
pub fn const_(self) -> Self {
if matches!(self, Self::Const(_)) {
self
} else {
Self::Const(Box::new(self))
}
}
#[inline]
#[must_use]
pub fn volatile_(self) -> Self {
if matches!(self, Self::Volatile(_)) {
self
} else {
Self::Volatile(Box::new(self))
}
}
#[inline]
#[must_use]
pub fn deref(self) -> Self {
match self {
Self::Pointer(inner) => *inner,
Self::Array { elem, .. } => *elem,
other => other,
}
}
#[inline]
#[must_use]
pub fn is_named(&self) -> bool {
matches!(self, Self::Named(_))
}
#[inline]
#[must_use]
pub fn as_named(&self) -> Option<&str> {
match self {
Self::Named(s) => Some(s),
_ => None,
}
}
#[inline]
#[must_use]
pub fn is_decl(&self) -> bool {
matches!(self, Self::Decl(_))
}
#[inline]
#[must_use]
pub fn as_decl(&self) -> Option<&str> {
match self {
Self::Decl(s) => Some(s),
_ => None,
}
}
#[inline]
#[must_use]
pub fn is_pointer(&self) -> bool {
matches!(self, Self::Pointer(_))
}
#[inline]
#[must_use]
pub fn as_pointer(&self) -> Option<&Self> {
match self {
Self::Pointer(inner) => Some(inner),
_ => None,
}
}
#[inline]
#[must_use]
pub fn is_array(&self) -> bool {
matches!(self, Self::Array { .. })
}
#[inline]
#[must_use]
pub fn as_array(&self) -> Option<(&Self, u64)> {
match self {
Self::Array { elem, len } => Some((elem, *len)),
_ => None,
}
}
#[inline]
#[must_use]
pub fn is_const(&self) -> bool {
matches!(self, Self::Const(_))
}
#[inline]
#[must_use]
pub fn is_volatile(&self) -> bool {
matches!(self, Self::Volatile(_))
}
#[inline]
#[must_use]
pub fn is_void(&self) -> bool {
matches!(self, Self::Void)
}
#[inline]
#[must_use]
pub fn is_scalar(&self) -> bool {
matches!(
self,
Self::Void | Self::Bool | Self::Int { .. } | Self::Float { .. }
)
}
#[inline]
#[must_use]
pub fn is_function(&self) -> bool {
matches!(self, Self::Function { .. })
}
#[inline]
#[must_use]
pub fn as_function(&self) -> Option<(&Self, &[Param], bool)> {
match self {
Self::Function {
ret,
params,
varargs,
..
} => Some((ret, params, *varargs)),
_ => None,
}
}
pub(crate) fn serialize(&self) -> Vec<u8> {
let mut buf = Vec::new();
self.encode(&mut buf);
buf
}
pub fn check(&self) -> Result<()> {
match self {
Self::Void
| Self::Bool
| Self::Int { .. }
| Self::Float { .. }
| Self::Bitfield { .. } => Ok(()),
Self::Named(name) => nul_checked(name, "name").map(drop),
Self::Decl(text) => nul_checked(text, "decl").map(drop),
Self::Pointer(inner) | Self::Const(inner) | Self::Volatile(inner) => inner.check(),
Self::Array { elem, .. } => elem.check(),
Self::Function { ret, params, .. } => {
ret.check()?;
for p in params {
if let Some(name) = &p.name {
nul_checked(name, "parameter name").map(drop)?;
}
p.ty.check()?;
}
Ok(())
}
}
}
pub(crate) fn checked_serialize(&self) -> Result<Vec<u8>> {
self.check()?;
Ok(self.serialize())
}
fn encode(&self, buf: &mut Vec<u8>) {
match self {
Self::Void => buf.push(sys::RECIPE_VOID),
Self::Bool => buf.push(sys::RECIPE_BOOL),
Self::Int { bytes, signed } => {
buf.push(sys::RECIPE_INT);
buf.push(*bytes);
buf.push(u8::from(*signed));
}
Self::Float { bytes } => {
buf.push(sys::RECIPE_FLOAT);
buf.push(*bytes);
}
Self::Bitfield {
nbytes,
width,
signed,
} => {
buf.push(sys::RECIPE_BITFIELD);
buf.push(*nbytes);
buf.push(*width);
buf.push(u8::from(*signed));
}
Self::Named(name) => encode_str(buf, sys::RECIPE_NAMED, name),
Self::Decl(text) => encode_str(buf, sys::RECIPE_DECL, text),
Self::Pointer(inner) => {
inner.encode(buf);
buf.push(sys::RECIPE_PTR);
}
Self::Array { elem, len } => {
elem.encode(buf);
buf.push(sys::RECIPE_ARRAY);
buf.extend_from_slice(&len.to_le_bytes());
}
Self::Const(inner) => {
inner.encode(buf);
buf.push(sys::RECIPE_CONST);
}
Self::Volatile(inner) => {
inner.encode(buf);
buf.push(sys::RECIPE_VOLATILE);
}
Self::Function {
ret,
params,
varargs,
cc,
} => {
ret.encode(buf);
for p in params {
p.ty.encode(buf);
}
buf.push(sys::RECIPE_FUNCTION);
let count = u32::try_from(params.len()).unwrap_or(u32::MAX);
buf.extend_from_slice(&count.to_le_bytes());
buf.push(u8::from(*varargs));
let cc_raw = cc.map_or(0u16, |c| u16::from(u8::from(c)));
buf.extend_from_slice(&cc_raw.to_le_bytes());
for p in params.iter().take(count as usize) {
encode_len_prefixed(buf, p.name.as_deref().unwrap_or(""));
}
}
}
}
}
fn encode_len_prefixed(buf: &mut Vec<u8>, s: &str) {
let bytes = s.as_bytes();
let len = u32::try_from(bytes.len()).unwrap_or(u32::MAX);
buf.extend_from_slice(&len.to_le_bytes());
buf.extend_from_slice(&bytes[..len as usize]);
}
fn encode_str(buf: &mut Vec<u8>, op: u8, s: &str) {
buf.push(op);
encode_len_prefixed(buf, s);
}
impl From<&str> for TypeExpr {
fn from(s: &str) -> Self {
if is_bare_type_name(s) {
Self::Named(s.to_owned())
} else {
Self::Decl(s.to_owned())
}
}
}
impl From<String> for TypeExpr {
fn from(s: String) -> Self {
if is_bare_type_name(&s) {
Self::Named(s)
} else {
Self::Decl(s)
}
}
}
impl fmt::Display for TypeExpr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Void => f.write_str("void"),
Self::Bool => f.write_str("bool"),
Self::Int { bytes, signed } => {
write!(
f,
"{}int{}",
if *signed { "" } else { "u" },
u32::from(*bytes) * 8
)
}
Self::Float { bytes } => write!(f, "float{}", u32::from(*bytes) * 8),
Self::Bitfield {
nbytes,
width,
signed,
} => write!(
f,
"{}int{}:{width}",
if *signed { "" } else { "u" },
u32::from(*nbytes) * 8
),
Self::Named(s) | Self::Decl(s) => f.write_str(s),
Self::Pointer(inner) => write!(f, "{inner} *"),
Self::Array { elem, len } => write!(f, "{elem}[{len}]"),
Self::Const(inner) => write!(f, "const {inner}"),
Self::Volatile(inner) => write!(f, "volatile {inner}"),
Self::Function {
ret,
params,
varargs,
..
} => {
write!(f, "{ret} (")?;
for (i, p) in params.iter().enumerate() {
if i != 0 {
f.write_str(", ")?;
}
match &p.name {
Some(name) => write!(f, "{} {name}", p.ty)?,
None => write!(f, "{}", p.ty)?,
}
}
if *varargs {
f.write_str(if params.is_empty() { "..." } else { ", ..." })?;
}
f.write_str(")")
}
}
}
}
fn is_bare_type_name(s: &str) -> bool {
!s.is_empty() && !is_builtin_type_keyword(s) && s.split("::").all(is_c_identifier)
}
fn is_builtin_type_keyword(s: &str) -> bool {
matches!(
s,
"void"
| "bool"
| "_Bool"
| "char"
| "short"
| "int"
| "long"
| "float"
| "double"
| "signed"
| "unsigned"
| "wchar_t"
| "__int8"
| "__int16"
| "__int32"
| "__int64"
| "__int128"
)
}
fn is_c_identifier(s: &str) -> bool {
let mut chars = s.chars();
match chars.next() {
Some(c) if c == '_' || c.is_ascii_alphabetic() => {}
_ => return false,
}
chars.all(|c| c == '_' || c.is_ascii_alphanumeric())
}
#[cfg(test)]
mod tests {
use assert2::assert;
use rstest::rstest;
use super::*;
const fn assert_send<T: Send>() {}
const _: () = assert_send::<TypeExpr>();
#[rstest]
#[case("Widget", true)]
#[case("_hidden", true)]
#[case("my_struct_t", true)]
#[case("Foo123", true)]
#[case("ns::Inner", true)]
#[case("a::b::c", true)]
#[case("int_t", true)]
#[case("int", false)]
#[case("char", false)]
#[case("void", false)]
#[case("unsigned", false)]
#[case("__int64", false)]
#[case("bool", false)]
#[case("Widget *", false)]
#[case("int[8]", false)]
#[case("struct pt", false)]
#[case("unsigned int", false)]
#[case("123Foo", false)]
#[case("", false)]
#[case("Widget ", false)]
#[case(" Widget", false)]
#[case("::Foo", false)]
#[case("a::", false)]
#[case("a:::b", false)]
fn classifies_bare_names_against_declarations(#[case] input: &str, #[case] bare: bool) {
assert!(is_bare_type_name(input) == bare);
}
#[rstest]
#[case("Widget", TypeExpr::Named("Widget".into()))]
#[case("ns::Inner", TypeExpr::Named("ns::Inner".into()))]
#[case("Widget *", TypeExpr::Decl("Widget *".into()))]
#[case("struct pt", TypeExpr::Decl("struct pt".into()))]
#[case("int", TypeExpr::Decl("int".into()))]
fn from_str_routes_by_classification(#[case] input: &str, #[case] expected: TypeExpr) {
assert!(TypeExpr::from(input) == expected);
}
#[test]
fn from_owned_string_matches_str_classification() {
assert!(TypeExpr::from("Widget".to_owned()) == TypeExpr::from("Widget"));
assert!(TypeExpr::from("Widget *".to_owned()) == TypeExpr::from("Widget *"));
}
#[test]
fn explicit_roots_bypass_classification() {
assert!(decl("Widget") == TypeExpr::Decl("Widget".into()));
assert!(named("Widget") == TypeExpr::Named("Widget".into()));
}
#[test]
fn scalar_roots_construct_leaves() {
assert!(void() == TypeExpr::Void);
assert!(bool_() == TypeExpr::Bool);
assert!(
int32()
== TypeExpr::Int {
bytes: 4,
signed: true
}
);
assert!(
uint8()
== TypeExpr::Int {
bytes: 1,
signed: false
}
);
assert!(float64() == TypeExpr::Float { bytes: 8 });
assert!(int32().is_scalar() && void().is_void());
assert!(!named("X").is_scalar());
}
#[test]
fn bitfield_root_constructs_a_leaf() {
assert!(
bitfield(4, 3, false)
== TypeExpr::Bitfield {
nbytes: 4,
width: 3,
signed: false
}
);
}
#[test]
fn pointer_and_array_stack_and_deref_peels() {
let pp = named("Foo").pointer().pointer();
assert!(pp.is_pointer());
assert!(pp.as_pointer() == Some(&named("Foo").pointer()));
assert!(pp.deref() == named("Foo").pointer());
let a = int32().array(8);
assert!(a.as_array() == Some((&int32(), 8)));
assert!(a.deref() == int32());
assert!(named("Foo").deref() == named("Foo"));
}
#[test]
fn qualifiers_are_idempotent_and_ordered() {
assert!(named("Foo").const_().const_() == named("Foo").const_());
assert!(int32().volatile_().volatile_() == int32().volatile_());
assert!(named("Foo").const_().is_const());
assert!(named("Foo").const_().pointer() != named("Foo").pointer().const_());
}
#[test]
fn projections_match_shape() {
let n = named("Widget");
assert!(n.is_named() && !n.is_decl());
assert!(n.as_named() == Some("Widget"));
assert!(n.as_decl().is_none());
assert!(n.as_pointer().is_none());
let d = decl("Widget *");
assert!(d.is_decl() && d.as_decl() == Some("Widget *"));
let p = named("Foo").pointer();
assert!(p.as_pointer() == Some(&named("Foo")));
assert!(p.as_array().is_none());
}
#[rstest]
#[case::void_leaf(void(), false, false, false, false, false, true)]
#[case::named_leaf(named("Widget"), true, false, false, false, false, false)]
#[case::decl_leaf(decl("Widget *"), false, false, false, false, false, false)]
#[case::pointer(named("Widget").pointer(), false, true, false, false, false, false)]
#[case::array(int32().array(8), false, false, true, false, false, false)]
#[case::const_qualified(named("Widget").const_(), false, false, false, true, false, false)]
#[case::volatile_qualified(named("Widget").volatile_(), false, false, false, false, true, false)]
#[case::const_of_pointer_checks_outer_layer_only(
named("Widget").pointer().const_(),
false, false, false, true, false, false
)]
fn predicate_truth_vector_covers_every_variant(
#[case] recipe: TypeExpr,
#[case] is_named: bool,
#[case] is_pointer: bool,
#[case] is_array: bool,
#[case] is_const: bool,
#[case] is_volatile: bool,
#[case] is_void: bool,
) {
assert!(recipe.is_named() == is_named);
assert!(recipe.is_pointer() == is_pointer);
assert!(recipe.is_array() == is_array);
assert!(recipe.is_const() == is_const);
assert!(recipe.is_volatile() == is_volatile);
assert!(recipe.is_void() == is_void);
}
#[test]
fn checked_serialize_matches_check_then_serialize() {
use crate::Error;
let recipe = named("Foo").pointer();
assert!(recipe.checked_serialize() == Ok(recipe.serialize()));
assert!(recipe.serialize() == vec![4, 3, 0, 0, 0, b'F', b'o', b'o', 6]);
assert!(let Err(Error::InteriorNul { arg: "name" }) = named("a\0b").checked_serialize());
}
#[test]
fn check_rejects_interior_nul_anywhere() {
use crate::Error;
assert!(let Err(Error::InteriorNul { arg: "name" }) = named("a\0b").check());
assert!(let Err(Error::InteriorNul { arg: "decl" }) = decl("int\0x").check());
assert!(
let Err(Error::InteriorNul { arg: "name" }) = named("a\0b").pointer().array(2).check()
);
let f = function(int32()).named_arg("p\0q", int32()).build();
assert!(let Err(Error::InteriorNul { arg: "parameter name" }) = f.check());
assert!(named("Foo").pointer().check().is_ok());
assert!(
function(int32())
.named_arg("ok", int32())
.build()
.check()
.is_ok()
);
}
#[test]
fn recipe_opcodes_pin_the_facade_mirror() {
assert!(sys::RECIPE_VOID == 0);
assert!(sys::RECIPE_BOOL == 1);
assert!(sys::RECIPE_INT == 2);
assert!(sys::RECIPE_FLOAT == 3);
assert!(sys::RECIPE_NAMED == 4);
assert!(sys::RECIPE_DECL == 5);
assert!(sys::RECIPE_PTR == 6);
assert!(sys::RECIPE_ARRAY == 7);
assert!(sys::RECIPE_CONST == 8);
assert!(sys::RECIPE_VOLATILE == 9);
assert!(sys::RECIPE_BITFIELD == 11);
}
#[rstest]
#[case(void(), vec![0])]
#[case(bool_(), vec![1])]
#[case(int32(), vec![2, 4, 1])]
#[case(uint8(), vec![2, 1, 0])]
#[case(float64(), vec![3, 8])]
#[case(bitfield(4, 3, false), vec![11, 4, 3, 0])]
#[case(named("Foo"), vec![4, 3, 0, 0, 0, b'F', b'o', b'o'])]
#[case(decl("T"), vec![5, 1, 0, 0, 0, b'T'])]
#[case(named("Foo").pointer(), vec![4, 3, 0, 0, 0, b'F', b'o', b'o', 6])]
#[case(int32().array(8), vec![2, 4, 1, 7, 8, 0, 0, 0, 0, 0, 0, 0])]
#[case(named("Foo").const_(), vec![4, 3, 0, 0, 0, b'F', b'o', b'o', 8])]
#[case(named("Foo").volatile_(), vec![4, 3, 0, 0, 0, b'F', b'o', b'o', 9])]
#[case(named("Foo").const_().pointer(), vec![4, 3, 0, 0, 0, b'F', b'o', b'o', 8, 6])]
#[case(named("Foo").pointer().const_(), vec![4, 3, 0, 0, 0, b'F', b'o', b'o', 6, 8])]
#[case(function(void()).build(), vec![0, 10, 0, 0, 0, 0, 0, 0, 0])]
#[case(function(int32()).arg(int32()).build(),
vec![2, 4, 1, 2, 4, 1, 10, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])]
#[case(function(int32()).named_arg("a", uint8()).variadic().build(),
vec![2, 4, 1, 2, 1, 0, 10, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, b'a'])]
fn serializes_to_postfix_bytecode(#[case] recipe: TypeExpr, #[case] expected: Vec<u8>) {
assert!(recipe.serialize() == expected);
}
#[rstest]
#[case(void(), "void")]
#[case(bool_(), "bool")]
#[case(int32(), "int32")]
#[case(int64(), "int64")]
#[case(uint8(), "uint8")]
#[case(uint32(), "uint32")]
#[case(float64(), "float64")]
#[case(bitfield(4, 3, false), "uint32:3")]
#[case(named("Foo"), "Foo")]
#[case(decl("Widget *"), "Widget *")]
#[case(named("Foo").pointer(), "Foo *")]
#[case(named("Foo").pointer().pointer(), "Foo * *")]
#[case(int32().array(8), "int32[8]")]
#[case(named("Foo").const_(), "const Foo")]
#[case(named("Foo").volatile_(), "volatile Foo")]
#[case(function(void()).build(), "void ()")]
#[case(function(int32()).variadic().build(), "int32 (...)")]
#[case(function(named("Foo").pointer()).arg(int32()).build(), "Foo * (int32)")]
#[case(
function(int32()).arg(int32()).named_arg("flags", uint32()).variadic().build(),
"int32 (int32, uint32 flags, ...)"
)]
fn display_renders_readable_c(#[case] recipe: TypeExpr, #[case] expected: &str) {
assert!(format!("{recipe}") == expected);
}
#[test]
fn function_builder_accumulates_params_and_flags() {
let f = function(int32())
.arg(int32())
.named_arg("flags", uint32())
.variadic()
.build();
let (ret, params, varargs) = f.as_function().expect("a function");
assert!(ret == &int32());
assert!(varargs);
assert!(params.len() == 2);
assert!(
params[0]
== Param {
name: None,
ty: int32()
}
);
assert!(
params[1]
== Param {
name: Some("flags".into()),
ty: uint32(),
}
);
let g = function(void()).build();
assert!(g.as_function() == Some((&void(), &[][..], false)));
assert!(g.is_function() && !int32().is_function());
}
#[test]
fn function_builder_into_typeexpr_needs_no_build() {
let via_into: TypeExpr = function(void()).arg(int32()).into();
assert!(via_into == function(void()).arg(int32()).build());
}
#[test]
fn calling_convention_reaches_the_recipe() {
let default = function(int32()).arg(int32()).build().serialize();
let stdcall = function(int32())
.arg(int32())
.calling_convention(CallingConvention::Stdcall)
.build()
.serialize();
assert!(
default != stdcall,
"a set cc must change the encoded recipe"
);
assert!(
stdcall.contains(&u8::from(CallingConvention::Stdcall))
&& !default.contains(&u8::from(CallingConvention::Stdcall)),
"the stdcall byte should appear only in the cc-carrying recipe"
);
}
mod proptests {
use proptest::prelude::*;
use super::*;
fn leaf_type_expr() -> impl Strategy<Value = TypeExpr> {
prop_oneof![
Just(int8()),
Just(int16()),
Just(int32()),
Just(int64()),
Just(uint8()),
Just(uint16()),
Just(uint32()),
Just(uint64()),
Just(bool_()),
Just(void()),
"[A-Za-z_][A-Za-z0-9_]{0,15}".prop_map(named),
]
}
fn identifier() -> impl Strategy<Value = String> {
"[A-Za-z_][A-Za-z0-9_]{0,15}"
}
fn qualified_name() -> impl Strategy<Value = String> {
prop::collection::vec(identifier(), 1..4).prop_map(|segs| segs.join("::"))
}
fn declarator_noise() -> impl Strategy<Value = char> {
prop_oneof![
Just(' '),
Just('*'),
Just('['),
Just(']'),
Just('.'),
Just('-')
]
}
fn oracle_is_bare_type_name(s: &str) -> bool {
const KEYWORDS: &[&str] = &[
"void", "bool", "_Bool", "char", "short", "int", "long", "float", "double",
"signed", "unsigned", "wchar_t", "__int8", "__int16", "__int32", "__int64",
"__int128",
];
fn is_identifier(seg: &str) -> bool {
let mut chars = seg.chars();
match chars.next() {
Some(c) if c == '_' || c.is_ascii_alphabetic() => {}
_ => return false,
}
chars.all(|c| c == '_' || c.is_ascii_alphanumeric())
}
!s.is_empty() && !KEYWORDS.contains(&s) && s.split("::").all(is_identifier)
}
proptest! {
#[test]
fn deref_inverts_pointer(base in leaf_type_expr()) {
prop_assert_eq!(base.clone().pointer().deref(), base);
}
#[test]
fn deref_inverts_array(base in leaf_type_expr(), len in any::<u64>()) {
prop_assert_eq!(base.clone().array(len).deref(), base);
}
#[test]
fn qualifiers_are_idempotent(base in leaf_type_expr()) {
prop_assert_eq!(base.clone().const_().const_(), base.clone().const_());
prop_assert_eq!(base.clone().volatile_().volatile_(), base.volatile_());
}
#[test]
fn bare_identifier_classifies_true(
name in qualified_name().prop_filter(
"exclude builtin keywords",
|s| !is_builtin_type_keyword(s),
),
) {
prop_assert!(is_bare_type_name(&name));
}
#[test]
fn is_bare_type_name_matches_oracle(name in qualified_name()) {
prop_assert_eq!(is_bare_type_name(&name), oracle_is_bare_type_name(&name));
}
#[test]
fn declarator_noise_classifies_false(
name in qualified_name(),
noise in declarator_noise(),
) {
let noisy = format!("{name}{noise}");
prop_assert!(!is_bare_type_name(&noisy));
}
}
}
}