diff --git a/ctest-test/build.rs b/ctest-test/build.rs index 5f6118bb94688..001be0ab20a66 100644 --- a/ctest-test/build.rs +++ b/ctest-test/build.rs @@ -19,19 +19,19 @@ fn test_ctest() { .header("t1.h") .include("src") .skip_private(true) - .rename_fn(|f| f.link_name().unwrap_or(f.ident()).to_string().into()) - .rename_static(|s| s.link_name().unwrap_or(s.ident()).to_string().into()) + .rename_fn(|f| f.link_name().unwrap_or(f.path()).to_string().into()) + .rename_static(|s| s.link_name().unwrap_or(s.path()).to_string().into()) .rename_union_ty(|ty| (ty == "T1Union").then_some(ty.to_string())) .rename_struct_ty(|ty| (ty == "Transparent").then_some(ty.to_string())) - .volatile_struct_field(|s, f| s.ident() == "V" && f.ident() == "v") - .volatile_static(|s| s.ident() == "vol_ptr") - .volatile_static(|s| s.ident() == "T1_fn_ptr_vol") - .volatile_fn_arg(|f, p| f.ident() == "T1_vol0" && p.ident() == "arg0") - .volatile_fn_arg(|f, p| f.ident() == "T1_vol2" && p.ident() == "arg1") - .volatile_fn_return_type(|f| f.ident() == "T1_vol1") - .volatile_fn_return_type(|f| f.ident() == "T1_vol2") + .volatile_struct_field(|s, f| s.path() == "V" && f.ident() == "v") + .volatile_static(|s| s.path() == "vol_ptr") + .volatile_static(|s| s.path() == "T1_fn_ptr_vol") + .volatile_fn_arg(|f, p| f.path() == "T1_vol0" && p.ident() == "arg0") + .volatile_fn_arg(|f, p| f.path() == "T1_vol2" && p.ident() == "arg1") + .volatile_fn_return_type(|f| f.path() == "T1_vol1") + .volatile_fn_return_type(|f| f.path() == "T1_vol2") // The parameter `a` of the functions `T1r`, `T1s`, `T1t`, `T1v` is an array. - .array_arg(|f, p| matches!(f.ident(), "T1r" | "T1s" | "T1t" | "T1v") && p.ident() == "a") + .array_arg(|f, p| matches!(f.path(), "T1r" | "T1s" | "T1t" | "T1v") && p.ident() == "a") .skip_roundtrip(|n| n == "Arr"); ctest::generate_test(&mut t1gen, "src/t1.rs", "t1gen.rs").unwrap(); diff --git a/ctest/src/ast/constant.rs b/ctest/src/ast/constant.rs index b14a0db6b0ee1..4b78b6efbe6bc 100644 --- a/ctest/src/ast/constant.rs +++ b/ctest/src/ast/constant.rs @@ -5,12 +5,27 @@ use crate::BoxStr; pub struct Const { pub(crate) public: bool, pub(crate) ident: BoxStr, + pub(crate) path: syn::Path, pub(crate) ty: syn::Type, } impl Const { - /// Return the identifier of the constant as a string. - pub fn ident(&self) -> &str { + /// Return the full path to the constant variable as a string. + /// + /// If inside a nested module, this will return a top-level-relative path, + /// but not a crate-relative path. For some item `foo` in module + /// `crate::bar`, the returned string will be `bar::foo`, and not + /// `crate::bar::foo`. + pub fn path(&self) -> &str { &self.ident } + + /// Returns the last path of the identifier, from the absolute path returned + /// by [`Const::path`]. + pub fn ident(&self) -> String { + let Some(syn::PathSegment { ident, .. }) = self.path.segments.last() else { + unreachable!("all parsed items have at least one element in their path") + }; + ident.to_string() + } } diff --git a/ctest/src/ast/function.rs b/ctest/src/ast/function.rs index e266a53efdbbd..60ee882ba8c1c 100644 --- a/ctest/src/ast/function.rs +++ b/ctest/src/ast/function.rs @@ -13,6 +13,7 @@ pub struct Fn { #[expect(unused)] pub(crate) abi: Abi, pub(crate) ident: BoxStr, + pub(crate) path: syn::Path, pub(crate) link_name: Option, #[expect(unused)] pub(crate) parameters: Vec, @@ -21,11 +22,25 @@ pub struct Fn { } impl Fn { - /// Return the identifier of the function as a string. - pub fn ident(&self) -> &str { + /// Return the full path to the function item as a string. + /// + /// If inside a nested module, this will return a top-level-relative path, + /// but not a crate-relative path. For some item `foo` in module + /// `crate::bar`, the returned string will be `bar::foo`, and not + /// `crate::bar::foo`. + pub fn path(&self) -> &str { &self.ident } + /// Returns the last path of the identifier, from the absolute path returned + /// by [`Fn::path`]. + pub fn ident(&self) -> String { + let Some(syn::PathSegment { ident, .. }) = self.path.segments.last() else { + unreachable!("all parsed items have at least one element in their path") + }; + ident.to_string() + } + /// Return the name of the function to be linked C side with. pub fn link_name(&self) -> Option<&str> { self.link_name.as_deref() diff --git a/ctest/src/ast/mod.rs b/ctest/src/ast/mod.rs index 325e05cb40056..816357f10971e 100644 --- a/ctest/src/ast/mod.rs +++ b/ctest/src/ast/mod.rs @@ -1,6 +1,7 @@ mod constant; mod field; mod function; +mod module; mod parameter; mod static_variable; mod structure; @@ -12,12 +13,31 @@ use std::fmt; pub use constant::Const; pub use field::Field; pub use function::Fn; +pub use module::Module; pub use parameter::Parameter; pub use static_variable::Static; pub use structure::Struct; pub use type_alias::Type; pub use union::Union; +use crate::MapInput; + +/// Transforms an item's absolute path to use `_` as path separator instead of +/// `::`. +pub(crate) fn escape_item_path<'a>(item: impl Into>) -> String { + let path = match item.into() { + MapInput::Struct(s) => s.path(), + MapInput::Union(u) => u.path(), + MapInput::Fn(f) => f.path(), + MapInput::Alias(a) => a.path(), + MapInput::Const(c) => c.path(), + MapInput::Static(s) => s.path(), + + _ => unimplemented!("other MapInput data constructors do not represent rust items"), + }; + path.replace("::", "_") +} + /// The ABI as defined by the extern block. #[derive(Debug, Clone, PartialEq, Eq)] pub enum Abi { diff --git a/ctest/src/ast/module.rs b/ctest/src/ast/module.rs new file mode 100644 index 0000000000000..faedb33c50137 --- /dev/null +++ b/ctest/src/ast/module.rs @@ -0,0 +1,33 @@ +use crate::BoxStr; +use crate::ffi_items::FfiItems; + +/// Represents a Rust module. `ctest` only considers items for which there is a +/// corresponding type in this crate. +#[derive(Debug, Clone)] +pub struct Module { + pub(crate) public: bool, + pub(crate) ident: BoxStr, + pub(crate) path: syn::Path, + pub(crate) items: FfiItems, +} + +impl Module { + /// Returns the full path to the module item. + /// + /// If inside a nested module, this will return a top-level-relative path, + /// but not a crate-relative path. For some item `foo` in module + /// `crate::bar`, the returned string will be `bar::foo`, and not + /// `crate::bar::foo`. + pub fn path(&self) -> &str { + &self.ident + } + + /// Returns the last path of the identifier, from the absolute path returned + /// by [`Module::path`]. + pub fn ident(&self) -> String { + let Some(syn::PathSegment { ident, .. }) = self.path.segments.last() else { + unreachable!("all parsed items have at least one element in their path") + }; + ident.to_string() + } +} diff --git a/ctest/src/ast/static_variable.rs b/ctest/src/ast/static_variable.rs index 665bd06eb8e4b..ab1797174b933 100644 --- a/ctest/src/ast/static_variable.rs +++ b/ctest/src/ast/static_variable.rs @@ -13,16 +13,31 @@ pub struct Static { #[expect(unused)] pub(crate) abi: Abi, pub(crate) ident: BoxStr, + pub(crate) path: syn::Path, pub(crate) link_name: Option, pub(crate) ty: syn::Type, } impl Static { - /// Return the identifier of the static variable as a string. - pub fn ident(&self) -> &str { + /// Return the full path to the static variable as a string. + /// + /// If inside a nested module, this will return a top-level-relative path, + /// but not a crate-relative path. For some item `foo` in module + /// `crate::bar`, the returned string will be `bar::foo`, and not + /// `crate::bar::foo`. + pub fn path(&self) -> &str { &self.ident } + /// Returns the last path of the identifier, from the absolute path returned + /// by [`Static::path`]. + pub fn ident(&self) -> String { + let Some(syn::PathSegment { ident, .. }) = self.path.segments.last() else { + unreachable!("all parsed items have at least one element in their path") + }; + ident.to_string() + } + /// Return the name of the function to be linked C side with. pub fn link_name(&self) -> Option<&str> { self.link_name.as_deref() diff --git a/ctest/src/ast/structure.rs b/ctest/src/ast/structure.rs index 1c935890d4d23..dc47731c44773 100644 --- a/ctest/src/ast/structure.rs +++ b/ctest/src/ast/structure.rs @@ -8,12 +8,27 @@ use crate::{ pub struct Struct { pub(crate) public: bool, pub(crate) ident: BoxStr, + pub(crate) path: syn::Path, pub(crate) fields: Vec, } impl Struct { - /// Return the identifier of the struct as a string. - pub fn ident(&self) -> &str { + /// Return the full path to the struct item as a string. + /// + /// If inside a nested module, this will return a top-level-relative path, + /// but not a crate-relative path. For some item `foo` in module + /// `crate::bar`, the returned string will be `bar::foo`, and not + /// `crate::bar::foo`. + pub fn path(&self) -> &str { &self.ident } + + /// Returns the last path of the identifier, from the absolute path returned + /// by [`Struct::path`]. + pub fn ident(&self) -> String { + let Some(syn::PathSegment { ident, .. }) = self.path.segments.last() else { + unreachable!("all parsed items have at least one element in their path") + }; + ident.to_string() + } } diff --git a/ctest/src/ast/type_alias.rs b/ctest/src/ast/type_alias.rs index 4947e934dde51..0d5c79a021938 100644 --- a/ctest/src/ast/type_alias.rs +++ b/ctest/src/ast/type_alias.rs @@ -5,12 +5,27 @@ use crate::BoxStr; pub struct Type { pub(crate) public: bool, pub(crate) ident: BoxStr, + pub(crate) path: syn::Path, pub(crate) ty: syn::Type, } impl Type { - /// Return the identifier of the type alias as a string. - pub fn ident(&self) -> &str { + /// Return the full path to the type alias as a string. + /// + /// If inside a nested module, this will return a top-level-relative path, + /// but not a crate-relative path. For some item `foo` in module + /// `crate::bar`, the returned string will be `bar::foo`, and not + /// `crate::bar::foo`. + pub fn path(&self) -> &str { &self.ident } + + /// Returns the last path of the identifier, from the absolute path returned + /// by [`Type::path`]. + pub fn ident(&self) -> String { + let Some(syn::PathSegment { ident, .. }) = self.path.segments.last() else { + unreachable!("all parsed items have at least one element in their path") + }; + ident.to_string() + } } diff --git a/ctest/src/ast/union.rs b/ctest/src/ast/union.rs index 990d6db0efca5..a49278d0de737 100644 --- a/ctest/src/ast/union.rs +++ b/ctest/src/ast/union.rs @@ -8,12 +8,27 @@ use crate::{ pub struct Union { pub(crate) public: bool, pub(crate) ident: BoxStr, + pub(crate) path: syn::Path, pub(crate) fields: Vec, } impl Union { - /// Return the identifier of the union as a string. - pub fn ident(&self) -> &str { + /// Return the full path to the union item as a string. + /// + /// If inside a nested module, this will return a top-level-relative path, + /// but not a crate-relative path. For some item `foo` in module + /// `crate::bar`, the returned string will be `bar::foo`, and not + /// `crate::bar::foo`. + pub fn path(&self) -> &str { &self.ident } + + /// Returns the last path of the identifier, from the absolute path returned + /// by [`Union::path`]. + pub fn ident(&self) -> String { + let Some(syn::PathSegment { ident, .. }) = self.path.segments.last() else { + unreachable!("all parsed items have at least one element in their path") + }; + ident.to_string() + } } diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index c7198082c946a..43e652c428524 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -1,9 +1,15 @@ //! Conversion of Rust code to a simplified abstract syntax tree. +use std::borrow::Borrow; use std::ops::Deref; +use quote::ToTokens; +use syn::Visibility; use syn::punctuated::Punctuated; -use syn::visit::Visit; +use syn::visit::{ + self, + Visit, +}; use crate::{ Abi, @@ -11,6 +17,7 @@ use crate::{ Const, Field, Fn, + Module, Parameter, Static, Struct, @@ -18,10 +25,13 @@ use crate::{ Union, }; -/// Represents a collected set of top-level Rust items relevant to FFI generation or analysis. +/// Represents a collected set of top-level Rust items relevant to FFI +/// generation or analysis. /// -/// Includes foreign functions/statics, type aliases, structs, unions, and constants. -#[derive(Default, Clone, Debug)] +/// Includes foreign functions/statics, type aliases, structs, unions, and +/// constants. Modules are collected as recursive `FfiItems`, and currently used +/// to narrow down tested items to those in a specific module. +#[derive(Clone, Debug)] pub(crate) struct FfiItems { pub(crate) aliases: Vec, pub(crate) structs: Vec, @@ -29,6 +39,12 @@ pub(crate) struct FfiItems { pub(crate) constants: Vec, pub(crate) foreign_functions: Vec, pub(crate) foreign_statics: Vec, + pub(crate) modules: Vec, + + /// This is used while recursing through parsed modules to gather absolute + /// paths to them as identifiers for both the modules and the items within + /// them. + current_module: syn::Path, } impl FfiItems { @@ -80,6 +96,48 @@ impl FfiItems { } } +impl Default for FfiItems { + fn default() -> Self { + Self { + aliases: Default::default(), + structs: Default::default(), + unions: Default::default(), + constants: Default::default(), + foreign_functions: Default::default(), + foreign_statics: Default::default(), + modules: Default::default(), + current_module: syn::Path { + leading_colon: None, + segments: Default::default(), + }, + } + } +} + +/// Appends a new module-local item to an absolute path that does *not* start +/// with `crate`. +/// +/// This is used whenever we need to create paths for either items or modules. +fn append_path(base: impl Borrow, new: impl Borrow) -> syn::Path { + let base = base.borrow(); + let new = new.borrow(); + if base.segments.is_empty() { + syn::parse_quote! { #new } + } else { + syn::parse_quote! { #base::#new } + } +} + +/// Returns a stringified `syn::Path` that is meant to match one-to-one the path +/// to the item from the crate root. +fn path_to_string(path: impl Borrow) -> BoxStr { + path.borrow() + .into_token_stream() + .to_string() + .replace(|c: char| c.is_ascii_whitespace(), "") + .into_boxed_str() +} + /// Determine whether an item is visible to other crates. /// /// This function assumes that if the visibility is restricted then it is not @@ -135,7 +193,8 @@ fn extract_single_link_name(attrs: &[syn::Attribute]) -> Option { fn visit_foreign_item_fn(table: &mut FfiItems, i: &syn::ForeignItemFn, abi: &Abi) { let public = is_visible(&i.vis); let abi = abi.clone(); - let ident = i.sig.ident.to_string().into_boxed_str(); + let path = append_path(&table.current_module, &i.sig.ident); + let ident = path_to_string(&path); let parameters = i .sig .inputs @@ -145,7 +204,10 @@ fn visit_foreign_item_fn(table: &mut FfiItems, i: &syn::ForeignItemFn, abi: &Abi ident: match arg.pat.deref() { syn::Pat::Ident(i) => i.ident.to_string().into_boxed_str(), _ => { - unimplemented!("Foreign functions are unlikely to have any other pattern.") + unimplemented!( + "Foreign functions are unlikely to have any other \ + pattern." + ) } }, ty: arg.ty.deref().clone(), @@ -165,6 +227,7 @@ fn visit_foreign_item_fn(table: &mut FfiItems, i: &syn::ForeignItemFn, abi: &Abi public, abi, ident, + path, link_name, parameters, return_type, @@ -174,7 +237,8 @@ fn visit_foreign_item_fn(table: &mut FfiItems, i: &syn::ForeignItemFn, abi: &Abi fn visit_foreign_item_static(table: &mut FfiItems, i: &syn::ForeignItemStatic, abi: &Abi) { let public = is_visible(&i.vis); let abi = abi.clone(); - let ident = i.ident.to_string().into_boxed_str(); + let path = append_path(&table.current_module, &i.ident); + let ident = path_to_string(&path); let ty = i.ty.deref().clone(); let link_name = extract_single_link_name(&i.attrs); @@ -182,6 +246,7 @@ fn visit_foreign_item_static(table: &mut FfiItems, i: &syn::ForeignItemStatic, a public, abi, ident, + path, link_name, ty, }); @@ -190,15 +255,22 @@ fn visit_foreign_item_static(table: &mut FfiItems, i: &syn::ForeignItemStatic, a impl<'ast> Visit<'ast> for FfiItems { fn visit_item_type(&mut self, i: &'ast syn::ItemType) { let public = is_visible(&i.vis); + let path = append_path(&self.current_module, &i.ident); + let ident = path_to_string(&path); let ty = i.ty.deref().clone(); - let ident = i.ident.to_string().into_boxed_str(); - self.aliases.push(Type { public, ident, ty }); + self.aliases.push(Type { + public, + ident, + path, + ty, + }); } fn visit_item_struct(&mut self, i: &'ast syn::ItemStruct) { let public = is_visible(&i.vis); - let ident = i.ident.to_string().into_boxed_str(); + let path = append_path(&self.current_module, &i.ident); + let ident = path_to_string(&path); let fields = match &i.fields { syn::Fields::Named(fields) => collect_fields(&fields.named), syn::Fields::Unnamed(fields) => collect_fields(&fields.unnamed), @@ -208,28 +280,37 @@ impl<'ast> Visit<'ast> for FfiItems { self.structs.push(Struct { public, ident, + path, fields, }); } fn visit_item_union(&mut self, i: &'ast syn::ItemUnion) { let public = is_visible(&i.vis); - let ident = i.ident.to_string().into_boxed_str(); + let path = append_path(&self.current_module, &i.ident); + let ident = path_to_string(&path); let fields = collect_fields(&i.fields.named); self.unions.push(Union { public, ident, + path, fields, }); } fn visit_item_const(&mut self, i: &'ast syn::ItemConst) { let public = is_visible(&i.vis); - let ident = i.ident.to_string().into_boxed_str(); + let path = append_path(&self.current_module, &i.ident); + let ident = path_to_string(&path); let ty = i.ty.deref().clone(); - self.constants.push(Const { public, ident, ty }); + self.constants.push(Const { + public, + ident, + path, + ty, + }); } fn visit_item_foreign_mod(&mut self, i: &'ast syn::ItemForeignMod) { @@ -253,4 +334,21 @@ impl<'ast> Visit<'ast> for FfiItems { } } } + + fn visit_item_mod(&mut self, i: &'ast syn::ItemMod) { + let syn::ItemMod { vis, ident, .. } = i; + let public = matches!(vis, Visibility::Public(_)); + let path = append_path(&self.current_module, ident); + let ident = path_to_string(&path); + let mut items = FfiItems::new(); + items.current_module = path.clone(); + visit::visit_item_mod(&mut items, i); + + self.modules.push(Module { + public, + ident, + path, + items, + }); + } } diff --git a/ctest/src/generator.rs b/ctest/src/generator.rs index 8f31af4488375..d1a914e35c566 100644 --- a/ctest/src/generator.rs +++ b/ctest/src/generator.rs @@ -26,6 +26,7 @@ use crate::{ Field, Language, MapInput, + Module, Parameter, Result, Static, @@ -40,8 +41,8 @@ use crate::{ /// The default Rust edition used to generate the code. const DEFAULT_EDITION: u32 = 2021; -/// A function that takes a mappable input and returns its mapping as `Some`, otherwise -/// use the default name if `None`. +/// A function that takes a mappable input and returns its mapping as `Some`, +/// otherwise use the default name if `None`. type MappedName = Rc Option>; /// A function that determines whether to skip an item or not. type Skip = Rc bool>; @@ -49,7 +50,8 @@ type Skip = Rc bool>; type VolatileItem = Rc bool>; /// A function that determines whether a function argument is an array. type ArrayArg = Rc bool>; -/// A function that determines whether to skip a test, taking in the identifier name. +/// A function that determines whether to skip a test, taking in the identifier +/// name. type SkipTest = Rc bool>; /// A function that determines whether a type alias is a c enum. type CEnum = Rc bool>; @@ -58,10 +60,10 @@ type CEnum = Rc bool>; #[derive(Clone, Default)] #[expect(missing_debug_implementations)] pub struct TestGenerator { - /// A vector of tuples, the left side being the header itself, and the right - /// being a list of defines that the header is associated with. Note that - /// these defines are only valid for the header, they are immediately undefined - /// afterwards. + /// A vector of tuples, the left side being the header itself, and the + /// right being a list of defines that the header is associated with. + /// Note that these defines are only valid for the header, they are + /// immediately undefined afterwards. pub(crate) headers: Vec<(BoxStr, Vec)>, /// The target that the tests run on. Defaults to the native target. pub(crate) target: Option, @@ -69,7 +71,8 @@ pub struct TestGenerator { pub(crate) includes: Vec, /// The directory to output the generated test files. out_dir: Option, - /// A list of flags to pass to the compiler with checking if they are supported. + /// A list of flags to pass to the compiler with checking if they are + /// supported. pub(crate) flags: Vec, /// A list of flags that are passed to the compiler if supported. pub(crate) flags_if_supported: Vec, @@ -79,7 +82,8 @@ pub struct TestGenerator { cfg: Vec<(String, Option)>, /// A list of functions that remaps names used in the tests. mapped_names: Vec, - /// Extra command line args to pass to cargo when generating macro expansions. + /// Extra command line args to pass to cargo when generating macro + /// expansions. macro_expansion_cargo_args: Vec, /// Crate name to use when performing macro expansion. crate_name: Option, @@ -93,7 +97,8 @@ pub struct TestGenerator { pub(crate) volatile_items: Vec, /// A list of functions that determine if an item is a C style enum. pub(crate) c_enums: Vec, - /// A list of functions that determine if a type is actually an array argument. + /// A list of functions that determine if a type is actually an array + /// argument. pub(crate) array_arg: Option, /// Whether to skip testing private items. pub(crate) skip_private: bool, @@ -110,19 +115,23 @@ pub struct TestGenerator { /// An error that occurs when generating the test files. #[derive(Debug, Error)] pub enum GenerationError { - /// An error that occurs when `rustc -Zunpretty=expand` fails to expand the crate. + /// An error that occurs when `rustc -Zunpretty=expand` fails to expand the + /// crate. #[error("unable to expand crate {0}: {1}")] MacroExpansion(PathBuf, String), - /// An error that occurs when `syn` is unable to parse the expanded crate due to invalid syntax. + /// An error that occurs when `syn` is unable to parse the expanded crate + /// due to invalid syntax. #[error("unable to parse expanded crate {0}: {1}")] RustSyntax(String, String), /// An error that occurs when the Rust to C translation fails. #[error("unable to prepare template input: {0}")] Translation(#[from] TranslationError), - /// An error that occurs when there are errors in the Rust side of the test template. + /// An error that occurs when there are errors in the Rust side of the test + /// template. #[error("unable to render Rust template: {0}")] RustTemplateRender(askama::Error), - /// An error that occurs when there are errors in the C side of the test template. + /// An error that occurs when there are errors in the C side of the test + /// template. #[error("unable to render C template: {0}")] CTemplateRender(askama::Error), #[error("unable to create or write template file: {0}")] @@ -151,20 +160,20 @@ impl TestGenerator { /// use ctest::TestGenerator; /// /// let mut cfg = TestGenerator::new(); - /// cfg.header("foo.h") - /// .header("bar.h"); + /// cfg.header("foo.h").header("bar.h"); /// ``` pub fn header(&mut self, header: &str) -> &mut Self { self.headers.push((header.into(), vec![])); self } - /// Add a header to be included as part of the generated C file, as well as defines for it. + /// Add a header to be included as part of the generated C file, as well as + /// defines for it. /// /// The generated C test will be compiled by a C compiler, and this can be /// used to ensure that all the necessary header files are included to test - /// all FFI definitions. The defines are only set for the inclusion of that header file, and are - /// undefined immediately after. + /// all FFI definitions. The defines are only set for the inclusion of that + /// header file, and are undefined immediately after. /// /// # Examples /// @@ -173,7 +182,7 @@ impl TestGenerator { /// /// let mut cfg = TestGenerator::new(); /// cfg.header_with_defines("foo.h", Vec::::new()) - /// .header_with_defines("bar.h", vec!["DEBUG", "DEPRECATED"]); + /// .header_with_defines("bar.h", vec!["DEBUG", "DEPRECATED"]); /// ``` pub fn header_with_defines( &mut self, @@ -189,13 +198,17 @@ impl TestGenerator { /// Sets the programming language, by default it is C. /// - /// This determines what compiler is chosen to compile the C/C++ tests, as well as adding - /// external linkage to the tests if set to C++, so that they can be used in Rust. + /// This determines what compiler is chosen to compile the C/C++ tests, as + /// well as adding external linkage to the tests if set to C++, so that + /// they can be used in Rust. /// /// # Examples /// /// ```no_run - /// use ctest::{TestGenerator, Language}; + /// use ctest::{ + /// Language, + /// TestGenerator, + /// }; /// /// let mut cfg = TestGenerator::new(); /// cfg.language(Language::CXX); @@ -244,7 +257,7 @@ impl TestGenerator { /// /// let mut cfg = TestGenerator::new(); /// cfg.cfg("foo", None) // cfg!(foo) - /// .cfg("bar", Some("baz")); // cfg!(bar = "baz") + /// .cfg("bar", Some("baz")); // cfg!(bar = "baz") /// ``` pub fn cfg(&mut self, k: &str, v: Option<&str>) -> &mut Self { self.cfg.push((k.to_string(), v.map(|s| s.to_string()))); @@ -336,6 +349,7 @@ impl TestGenerator { /// Indicate that a type alias is actually a C enum. /// /// # Examples + /// /// ```no_run /// use ctest::TestGenerator; /// @@ -352,7 +366,10 @@ impl TestGenerator { /// # Examples /// /// ```no_run - /// use ctest::{TestGenerator, VolatileItemKind}; + /// use ctest::{ + /// TestGenerator, + /// VolatileItemKind, + /// }; /// /// let mut cfg = TestGenerator::new(); /// cfg.volatile_struct_field(|s, f| { @@ -378,12 +395,13 @@ impl TestGenerator { /// # Examples /// /// ```no_run - /// use ctest::{TestGenerator, VolatileItemKind}; + /// use ctest::{ + /// TestGenerator, + /// VolatileItemKind, + /// }; /// /// let mut cfg = TestGenerator::new(); - /// cfg.volatile_static(|s| { - /// s.ident() == "foo_t" - /// }); + /// cfg.volatile_static(|s| s.ident() == "foo_t"); /// ``` pub fn volatile_static(&mut self, f: impl Fn(Static) -> bool + 'static) -> &mut Self { self.volatile_items.push(Rc::new(move |item| { @@ -401,12 +419,13 @@ impl TestGenerator { /// # Examples /// /// ```no_run - /// use ctest::{TestGenerator, VolatileItemKind}; + /// use ctest::{ + /// TestGenerator, + /// VolatileItemKind, + /// }; /// /// let mut cfg = TestGenerator::new(); - /// cfg.volatile_fn_arg(|f, _p| { - /// f.ident() == "size_of_T" - /// }); + /// cfg.volatile_fn_arg(|f, _p| f.ident() == "size_of_T"); /// ``` pub fn volatile_fn_arg( &mut self, @@ -427,12 +446,13 @@ impl TestGenerator { /// # Examples /// /// ```no_run - /// use ctest::{TestGenerator, VolatileItemKind}; + /// use ctest::{ + /// TestGenerator, + /// VolatileItemKind, + /// }; /// /// let mut cfg = TestGenerator::new(); - /// cfg.volatile_fn_return_type(|f| { - /// f.ident() == "size_of_T" - /// }); + /// cfg.volatile_fn_return_type(|f| f.ident() == "size_of_T"); /// ``` pub fn volatile_fn_return_type( &mut self, @@ -450,8 +470,8 @@ impl TestGenerator { /// Indicate that a function pointer argument is an array. /// - /// This closure should return true if a pointer argument to a function should be generated - /// with `T foo[]` syntax rather than `T *foo`. + /// This closure should return true if a pointer argument to a function + /// should be generated with `T foo[]` syntax rather than `T *foo`. /// /// # Examples /// @@ -459,17 +479,42 @@ impl TestGenerator { /// use ctest::TestGenerator; /// /// let mut cfg = TestGenerator::new(); - /// cfg.array_arg(|func, arg| { - /// match (func.ident(), arg.ident()) { - /// ("foo", "bar") => true, - /// _ => false, - /// }}); + /// cfg.array_arg(|func, arg| match (func.ident().as_str(), arg.ident()) { + /// ("foo", "bar") => true, + /// _ => false, + /// }); /// ``` pub fn array_arg(&mut self, f: impl Fn(crate::Fn, Parameter) -> bool + 'static) -> &mut Self { self.array_arg = Some(Rc::new(f)); self } + /// Skip a specific module in the crate. + /// + /// Module paths are given relative to the crate root, so for example the + /// identifier of a module `bar` inside a top-level module `foo` would be + /// `foo::bar`, and not `crate::foo::bar`. This is returned by the + /// [`Module::path`] function. + /// + /// # Examples + /// + /// ```no_run + /// use ctest::TestGenerator; + /// + /// let mut cfg = TestGenerator::new(); + /// cfg.skip_module(|module| module.ident() == "foo::bar"); + /// ``` + pub fn skip_module(&mut self, f: impl Fn(&Module) -> bool + 'static) -> &mut Self { + self.skips.push(Rc::new(move |item| { + if let MapInput::Module(module) = item { + f(module) + } else { + false + } + })); + self + } + /// Configures whether the tests for a struct are emitted. /// /// # Examples @@ -478,9 +523,7 @@ impl TestGenerator { /// use ctest::TestGenerator; /// /// let mut cfg = TestGenerator::new(); - /// cfg.skip_struct(|s| { - /// s.ident().starts_with("foo_") - /// }); + /// cfg.skip_struct(|s| s.ident().starts_with("foo_")); /// ``` pub fn skip_struct(&mut self, f: impl Fn(&Struct) -> bool + 'static) -> &mut Self { self.skips.push(Rc::new(move |item| { @@ -501,9 +544,7 @@ impl TestGenerator { /// use ctest::TestGenerator; /// /// let mut cfg = TestGenerator::new(); - /// cfg.skip_union(|u| { - /// u.ident().starts_with("foo_") - /// }); + /// cfg.skip_union(|u| u.ident().starts_with("foo_")); /// ``` pub fn skip_union(&mut self, f: impl Fn(&Union) -> bool + 'static) -> &mut Self { self.skips.push(Rc::new(move |item| { @@ -573,9 +614,7 @@ impl TestGenerator { /// use ctest::TestGenerator; /// /// let mut cfg = TestGenerator::new(); - /// cfg.skip_alias(|a| { - /// a.ident().starts_with("foo_") - /// }); + /// cfg.skip_alias(|a| a.ident().starts_with("foo_")); /// ``` pub fn skip_alias(&mut self, f: impl Fn(&Type) -> bool + 'static) -> &mut Self { self.skips.push(Rc::new(move |item| { @@ -596,9 +635,7 @@ impl TestGenerator { /// use ctest::TestGenerator; /// /// let mut cfg = TestGenerator::new(); - /// cfg.skip_const(|s| { - /// s.ident().starts_with("FOO_") - /// }); + /// cfg.skip_const(|s| s.ident().starts_with("FOO_")); /// ``` pub fn skip_const(&mut self, f: impl Fn(&Const) -> bool + 'static) -> &mut Self { self.skips.push(Rc::new(move |item| { @@ -619,9 +656,7 @@ impl TestGenerator { /// use ctest::TestGenerator; /// /// let mut cfg = TestGenerator::new(); - /// cfg.skip_static(|s| { - /// s.ident().starts_with("foo_") - /// }); + /// cfg.skip_static(|s| s.ident().starts_with("foo_")); /// ``` pub fn skip_static(&mut self, f: impl Fn(&Static) -> bool + 'static) -> &mut Self { self.skips.push(Rc::new(move |item| { @@ -642,9 +677,7 @@ impl TestGenerator { /// use ctest::TestGenerator; /// /// let mut cfg = TestGenerator::new(); - /// cfg.skip_fn(|s| { - /// s.ident().starts_with("foo_") - /// }); + /// cfg.skip_fn(|s| s.ident().starts_with("foo_")); /// ``` pub fn skip_fn(&mut self, f: impl Fn(&crate::Fn) -> bool + 'static) -> &mut Self { self.skips.push(Rc::new(move |item| { @@ -659,8 +692,9 @@ impl TestGenerator { /// Configures whether tests for a C enum are generated. /// - /// A C enum consists of a type alias, as well as constants that have the same type. Tests - /// for both the alias as well as the constants are skipped. + /// A C enum consists of a type alias, as well as constants that have the + /// same type. Tests for both the alias as well as the constants are + /// skipped. /// /// # Examples /// @@ -710,8 +744,9 @@ impl TestGenerator { /// Set a `-D` flag for the C compiler being called. /// - /// This can be used to define various global variables to configure how header - /// files are included or what APIs are exposed from header files. + /// This can be used to define various global variables to configure how + /// header files are included or what APIs are exposed from header + /// files. /// /// # Examples /// @@ -719,8 +754,7 @@ impl TestGenerator { /// use ctest::TestGenerator; /// /// let mut cfg = TestGenerator::new(); - /// cfg.define("_GNU_SOURCE", None) - /// .define("_WIN32_WINNT", Some("0x8000")); + /// cfg.define("_GNU_SOURCE", None).define("_WIN32_WINNT", Some("0x8000")); /// ``` pub fn define(&mut self, k: &str, v: Option<&str>) -> &mut Self { self.global_defines @@ -736,9 +770,9 @@ impl TestGenerator { /// Configures the crate name which should be used during macro expansion. /// - /// If the tested crate uses `#![crate_name = "..."]`, this must be called with the - /// same name. Otherwise, there will be an error about `--crate-name` not - /// matching. + /// If the tested crate uses `#![crate_name = "..."]`, this must be called + /// with the same name. Otherwise, there will be an error about + /// `--crate-name` not matching. pub fn crate_name(&mut self, name: String) -> &mut Self { self.crate_name = Some(name); self @@ -822,9 +856,7 @@ impl TestGenerator { /// use ctest::TestGenerator; /// /// let mut cfg = TestGenerator::new(); - /// cfg.rename_constant(|c| { - /// (c.ident() == "FOO").then_some("BAR".to_string()) - /// }); + /// cfg.rename_constant(|c| (c.ident() == "FOO").then_some("BAR".to_string())); /// ``` pub fn rename_constant(&mut self, f: impl Fn(&Const) -> Option + 'static) -> &mut Self { self.mapped_names.push(Rc::new(move |item| { @@ -962,9 +994,7 @@ impl TestGenerator { /// use ctest::TestGenerator; /// /// let mut cfg = TestGenerator::new(); - /// cfg.rename_type(|ty| { - /// Some(format!("{}_t", ty)) - /// }); + /// cfg.rename_type(|ty| Some(format!("{}_t", ty))); /// ``` pub fn rename_type(&mut self, f: impl Fn(&str) -> Option + 'static) -> &mut Self { self.mapped_names.push(Rc::new(move |item| { @@ -989,9 +1019,7 @@ impl TestGenerator { /// use ctest::TestGenerator; /// /// let mut cfg = TestGenerator::new(); - /// cfg.rename_struct_ty(|ty| { - /// (ty == "timeval").then(|| format!("{ty}_t")) - /// }); + /// cfg.rename_struct_ty(|ty| (ty == "timeval").then(|| format!("{ty}_t"))); /// ``` pub fn rename_struct_ty(&mut self, f: impl Fn(&str) -> Option + 'static) -> &mut Self { self.mapped_names.push(Rc::new(move |item| { @@ -1016,9 +1044,7 @@ impl TestGenerator { /// use ctest::TestGenerator; /// /// let mut cfg = TestGenerator::new(); - /// cfg.rename_struct_ty(|ty| { - /// (ty == "T1Union").then(|| format!("__{ty}")) - /// }); + /// cfg.rename_struct_ty(|ty| (ty == "T1Union").then(|| format!("__{ty}"))); /// ``` pub fn rename_union_ty(&mut self, f: impl Fn(&str) -> Option + 'static) -> &mut Self { self.mapped_names.push(Rc::new(move |item| { @@ -1045,9 +1071,7 @@ impl TestGenerator { /// use ctest::TestGenerator; /// /// let mut cfg = TestGenerator::new(); - /// cfg.skip_roundtrip(|s| { - /// s.starts_with("foo_") - /// }); + /// cfg.skip_roundtrip(|s| s.starts_with("foo_")); /// ``` pub fn skip_roundtrip(&mut self, f: impl Fn(&str) -> bool + 'static) -> &mut Self { self.skip_roundtrip = Some(Rc::new(f)); @@ -1067,9 +1091,7 @@ impl TestGenerator { /// use ctest::TestGenerator; /// /// let mut cfg = TestGenerator::new(); - /// cfg.skip_signededness(|s| { - /// s.starts_with("foo_") - /// }); + /// cfg.skip_signededness(|s| s.starts_with("foo_")); /// ``` pub fn skip_signededness(&mut self, f: impl Fn(&str) -> bool + 'static) -> &mut Self { self.skip_signededness = Some(Rc::new(f)); @@ -1163,19 +1185,20 @@ impl TestGenerator { Ok(output_file_path) } - /// Maps Rust identifiers or types to C counterparts, or defaults to the original name. + /// Maps Rust identifiers or types to C counterparts, or defaults to the + /// original name. pub(crate) fn rty_to_cty<'a>(&self, item: impl Into>) -> String { let item = item.into(); if let Some(mapped) = self.mapped_names.iter().find_map(|f| f(&item)) { return mapped; } match item { - MapInput::Const(c) => c.ident().to_string(), - MapInput::Fn(f) => f.ident().to_string(), - MapInput::Static(s) => s.ident().to_string(), - MapInput::Struct(s) => s.ident().to_string(), - MapInput::Union(u) => u.ident().to_string(), - MapInput::Alias(t) => t.ident().to_string(), + MapInput::Const(c) => c.ident(), + MapInput::Fn(f) => f.ident(), + MapInput::Static(s) => s.ident(), + MapInput::Struct(s) => s.ident(), + MapInput::Union(u) => u.ident(), + MapInput::Alias(t) => t.ident(), MapInput::StructField(_, f) => f.ident().to_string(), MapInput::UnionField(_, f) => f.ident().to_string(), MapInput::StructType(ty) => format!("struct {ty}"), @@ -1184,6 +1207,10 @@ impl TestGenerator { MapInput::StructFieldType(_, f) => f.ident().to_string(), MapInput::UnionFieldType(_, f) => f.ident().to_string(), MapInput::Type(ty) => translate_primitive_type(ty), + + MapInput::Module(_) => { + unreachable!("modules don't get tested on the c side of things") + } } } } diff --git a/ctest/src/lib.rs b/ctest/src/lib.rs index c03f9de54d524..0e9da383e3bcc 100644 --- a/ctest/src/lib.rs +++ b/ctest/src/lib.rs @@ -27,6 +27,7 @@ pub use ast::{ Const, Field, Fn, + Module, Parameter, Static, Struct, @@ -90,6 +91,7 @@ pub(crate) enum MapInput<'a> { CEnumType(&'a str), StructFieldType(&'a Struct, &'a Field), UnionFieldType(&'a Union, &'a Field), + Module(&'a Module), } /// The language used to generate the tests. @@ -167,3 +169,9 @@ impl<'a> From<&'a Union> for MapInput<'a> { MapInput::Union(u) } } + +impl<'a> From<&'a Module> for MapInput<'a> { + fn from(value: &'a Module) -> Self { + MapInput::Module(value) + } +} diff --git a/ctest/src/template.rs b/ctest/src/template.rs index 4ed9fe559e52f..7bb48285152f4 100644 --- a/ctest/src/template.rs +++ b/ctest/src/template.rs @@ -19,6 +19,7 @@ use crate::{ TestGenerator, TranslationError, VolatileItemKind, + ast, cdecl, }; @@ -93,7 +94,8 @@ pub(crate) struct TestTemplate { } impl TestTemplate { - /// Populate all tests for all items depending on the configuration provided. + /// Populate all tests for all items depending on the configuration + /// provided. pub(crate) fn new( ffi_items: &FfiItems, generator: &TestGenerator, @@ -113,7 +115,8 @@ impl TestTemplate { Ok(template) } - /// Populates tests for constants and C-str constants, keeping track of the names of each test. + /// Populates tests for constants and C-str constants, keeping track of the + /// names of each test. fn populate_const_and_cstr_tests( &mut self, helper: &TranslateHelper, @@ -125,18 +128,27 @@ impl TestTemplate { && matches!(ptr.mutability, syn::PointerMutability::Const(_)) { let item = TestCStr { + // [NOTE]: the test name needs the full path to the item + // with path separators escaped as `_`. + test_name: cstr_test_ident(&ast::escape_item_path(constant)), + // [NOTE]: the item's identifier ought be the last segment + // of its path because it is used in the C tests. id: constant.ident().into(), - test_name: cstr_test_ident(constant.ident()), - rust_val: constant.ident().into(), + // [NOTE]: the item's Rust identifier ought be the full, + // unescaped path to the item (as it itself is not + // used as an identifier for some other item in the + // generated tests but as a standalone identifier.) + rust_val: constant.path().into(), c_val: helper.c_ident(constant).into(), }; self.const_cstr_tests.push(item.clone()); self.test_idents.push(item.test_name); } else { + // [NOTE]: see the above notes. let item = TestConst { id: constant.ident().into(), - test_name: const_test_ident(constant.ident()), - rust_val: constant.ident().into(), + test_name: const_test_ident(&ast::escape_item_path(constant)), + rust_val: constant.path().into(), rust_ty: constant.ty.to_token_stream().to_string().into_boxed_str(), c_val: helper.c_ident(constant).into(), c_ty: helper.c_type(constant)?.into(), @@ -158,9 +170,9 @@ impl TestTemplate { ) -> Result<(), TranslationError> { for alias in helper.filtered_ffi_items.aliases() { let item = TestSizeAlign { - test_name: size_align_test_ident(alias.ident()), + test_name: size_align_test_ident(&ast::escape_item_path(alias)), id: alias.ident().into(), - rust_ty: alias.ident().into(), + rust_ty: alias.path().into(), c_ty: helper.c_type(alias)?.into(), }; self.size_align_tests.push(item.clone()); @@ -168,9 +180,9 @@ impl TestTemplate { } for struct_ in helper.filtered_ffi_items.structs() { let item = TestSizeAlign { - test_name: size_align_test_ident(struct_.ident()), + test_name: size_align_test_ident(&ast::escape_item_path(struct_)), id: struct_.ident().into(), - rust_ty: struct_.ident().into(), + rust_ty: struct_.path().into(), c_ty: helper.c_type(struct_)?.into(), }; self.size_align_tests.push(item.clone()); @@ -178,9 +190,9 @@ impl TestTemplate { } for union_ in helper.filtered_ffi_items.unions() { let item = TestSizeAlign { - test_name: size_align_test_ident(union_.ident()), + test_name: size_align_test_ident(&ast::escape_item_path(union_)), id: union_.ident().into(), - rust_ty: union_.ident().into(), + rust_ty: union_.path().into(), c_ty: helper.c_type(union_)?.into(), }; self.size_align_tests.push(item.clone()); @@ -202,13 +214,13 @@ impl TestTemplate { .generator .skip_signededness .as_ref() - .is_some_and(|skip| skip(alias.ident())); + .is_some_and(|skip| skip(alias.path())); if !helper.translator.is_signed(&alias.ty) || should_skip_signededness_test { continue; } let item = TestSignededness { - test_name: signededness_test_ident(alias.ident()), + test_name: signededness_test_ident(&ast::escape_item_path(alias)), id: alias.ident().into(), c_ty: helper.c_type(alias)?.into(), }; @@ -238,7 +250,7 @@ impl TestTemplate { }) .map(|(struct_, field)| { ( - struct_.ident(), + MapInput::Struct(struct_), field, helper.c_type(struct_), helper.c_ident(MapInput::StructField(struct_, field)), @@ -254,19 +266,29 @@ impl TestTemplate { }) .map(|(union_, field)| { ( - union_.ident(), + MapInput::Union(union_), field, helper.c_type(union_), helper.c_ident(MapInput::UnionField(union_, field)), ) }); - for (id, field, c_ty, c_field) in struct_fields.chain(union_fields) { + for (ty, field, c_ty, c_field) in struct_fields.chain(union_fields) { + let (escaped_path, id, path) = match ty { + MapInput::Struct(s) => (ast::escape_item_path(s), s.ident(), s.path()), + MapInput::Union(u) => (ast::escape_item_path(u), u.ident(), u.path()), + + _ => unreachable!( + "we consider only records and untagged unions in field \ + size/offset tests" + ), + }; let item = TestFieldSizeOffset { - test_name: field_size_offset_test_ident(id, field.ident()), + test_name: field_size_offset_test_ident(&escaped_path, field.ident()), id: id.into(), - c_ty: c_ty?.into(), field: field.clone(), + rust_ty: path.into(), + c_ty: c_ty?.into(), c_field: c_field.into_boxed_str(), }; self.field_size_offset_tests.push(item.clone()); @@ -288,16 +310,13 @@ impl TestTemplate { if let syn::Type::Array(_) = alias.ty { continue; } - let c_ty = helper.c_type(alias)?; - self.add_roundtrip_test(helper, alias.ident(), &[], &c_ty, true); + self.add_roundtrip_test(helper, MapInput::Alias(alias), &[], true)?; } for struct_ in helper.filtered_ffi_items.structs() { - let c_ty = helper.c_type(struct_)?; - self.add_roundtrip_test(helper, struct_.ident(), &struct_.fields, &c_ty, false); + self.add_roundtrip_test(helper, MapInput::Struct(struct_), &struct_.fields, false)?; } for union_ in helper.filtered_ffi_items.unions() { - let c_ty = helper.c_type(union_)?; - self.add_roundtrip_test(helper, union_.ident(), &union_.fields, &c_ty, false); + self.add_roundtrip_test(helper, MapInput::Union(union_), &union_.fields, false)?; } Ok(()) @@ -306,27 +325,54 @@ impl TestTemplate { fn add_roundtrip_test( &mut self, helper: &TranslateHelper, - ident: &str, + input: MapInput, fields: &[Field], - c_ty: &str, is_alias: bool, - ) { + ) -> Result<(), TranslationError> { + let (escaped_path, ident, path, c_ty) = match input { + MapInput::Struct(s) => ( + ast::escape_item_path(s), + s.ident(), + s.path(), + helper.c_type(s)?, + ), + MapInput::Union(u) => ( + ast::escape_item_path(u), + u.ident(), + u.path(), + helper.c_type(u)?, + ), + MapInput::Alias(a) => ( + ast::escape_item_path(a), + a.ident(), + a.path(), + helper.c_type(a)?, + ), + + _ => unreachable!("other rust items are not tested for roundtrip tests"), + }; let should_skip_roundtrip_test = helper .generator .skip_roundtrip .as_ref() - .is_some_and(|skip| skip(ident)); + .is_some_and(|skip| skip(path)); if !should_skip_roundtrip_test { let item = TestRoundtrip { - test_name: roundtrip_test_ident(ident), + test_name: roundtrip_test_ident(&escaped_path), id: ident.into(), - fields: fields.iter().filter(|f| f.public).cloned().collect(), + fields: fields + .iter() + .filter(|Field { public, .. }| *public) + .cloned() + .collect(), + rust_ty: path.into(), c_ty: c_ty.into(), is_alias, }; self.roundtrip_tests.push(item.clone()); self.test_idents.push(item.test_name); } + Ok(()) } /// Populates field tests for structs/unions. @@ -350,16 +396,15 @@ impl TestTemplate { }) .map(|(s, f)| { ( - s.ident(), + MapInput::Struct(s), f, helper.c_type(s), helper.c_ident(MapInput::StructField(s, f)), - if !helper.generator.volatile_items.is_empty() - && helper - .generator - .volatile_items - .iter() - .any(|vf| vf(VolatileItemKind::StructField(s.clone(), f.clone()))) + if helper + .generator + .volatile_items + .iter() + .any(|vf| vf(VolatileItemKind::StructField(s.clone(), f.clone()))) { "volatile " } else { @@ -379,7 +424,7 @@ impl TestTemplate { }) .map(|(u, f)| { ( - u.ident(), + MapInput::Union(u), f, helper.c_type(u), helper.c_ident(MapInput::UnionField(u, f)), @@ -387,7 +432,16 @@ impl TestTemplate { ) }); - for (id, field, c_ty, c_field, volatile_keyword) in struct_fields.chain(union_fields) { + for (ty, field, c_ty, c_field, volatile_keyword) in struct_fields.chain(union_fields) { + let (escaped_path, id, path) = match ty { + MapInput::Struct(s) => (ast::escape_item_path(s), s.ident(), s.path()), + MapInput::Union(u) => (ast::escape_item_path(u), u.ident(), u.path()), + + _ => unreachable!( + "we consider only records and untagged unions in field \ + size/offset tests" + ), + }; let field_return_type = cdecl::cdecl( &cdecl::ptr( helper.translator.translate_type(&field.ty)?, @@ -404,10 +458,11 @@ impl TestTemplate { })? .into_boxed_str(); let item = TestFieldPtr { - test_name: field_ptr_test_ident(id, field.ident()), + test_name: field_ptr_test_ident(&escaped_path, field.ident()), id: id.into(), c_ty: c_ty?.into(), field: field.clone(), + rust_ty: path.into(), c_field: c_field.into_boxed_str(), volatile_keyword: volatile_keyword.into(), field_return_type, @@ -426,22 +481,23 @@ impl TestTemplate { &mut self, helper: &TranslateHelper, ) -> Result<(), TranslationError> { - let should_skip_fn_test = |ident| { + let should_skip_fn_test = |path| { helper .generator .skip_fn_ptrcheck .as_ref() - .is_some_and(|skip| skip(ident)) + .is_some_and(|skip| skip(path)) }; for func in helper.filtered_ffi_items.foreign_functions() { - if should_skip_fn_test(func.ident()) { + if should_skip_fn_test(func.path()) { continue; } let item = TestForeignFn { - test_name: foreign_fn_test_ident(func.ident()), + test_name: foreign_fn_test_ident(&ast::escape_item_path(func)), id: func.ident().into(), - c_val: helper.c_ident(func).into_boxed_str(), + rust_ty: func.path().into(), + c_val: helper.c_ident(func).into(), }; self.foreign_fn_tests.push(item.clone()); @@ -451,7 +507,8 @@ impl TestTemplate { Ok(()) } - /// Populates tests for foreign statics, keeping track of the names of each test. + /// Populates tests for foreign statics, keeping track of the names of each + /// test. fn populate_foreign_static_tests( &mut self, helper: &TranslateHelper, @@ -460,10 +517,11 @@ impl TestTemplate { let rust_ty = static_.ty.to_token_stream().to_string().into_boxed_str(); let item = TestForeignStatic { - test_name: static_test_ident(static_.ident()), + test_name: static_test_ident(&ast::escape_item_path(static_)), id: static_.ident().into(), c_val: helper.c_ident(static_).into_boxed_str(), rust_ty, + rust_val: static_.path().into(), }; self.foreign_static_tests.push(item.clone()); @@ -477,12 +535,15 @@ impl TestTemplate { /* Many test structures have the following fields: * * - `test_name`: The function name. - * - `id`: An identifier that can be used to create functions related to this type without conflict, - * usually also part of `test_name`. - * - `rust_val`: Identifier for a Rust value, with path qualifications if needed. - * - `rust_ty`: The Rust type of the relevant item, with path qualifications if needed. - * - `c_val`: Identifier for a C value (e.g. `#define`) - * - `c_ty`: The C type of the constant, qualified with `struct` or `union` if needed. + * - `id` : An identifier that can be used to create functions related + * to this type without conflict, usually also part of `test_name`. + * - `rust_val` : Identifier for a Rust value, with path qualifications if + * needed. + * - `rust_ty` : The Rust type of the relevant item, with path + * qualifications if needed. + * - `c_val` : Identifier for a C value (e.g. `#define`) + * - `c_ty` : The C type of the constant, qualified with `struct` or + * `union` if needed. */ #[derive(Clone, Debug)] @@ -525,6 +586,7 @@ pub(crate) struct TestFieldPtr { pub test_name: BoxStr, pub id: BoxStr, pub field: Field, + pub rust_ty: BoxStr, pub c_field: BoxStr, pub c_ty: BoxStr, pub volatile_keyword: BoxStr, @@ -536,6 +598,7 @@ pub(crate) struct TestFieldSizeOffset { pub test_name: BoxStr, pub id: BoxStr, pub field: Field, + pub rust_ty: BoxStr, pub c_field: BoxStr, pub c_ty: BoxStr, } @@ -545,6 +608,7 @@ pub(crate) struct TestRoundtrip { pub test_name: BoxStr, pub id: BoxStr, pub fields: Vec, + pub rust_ty: BoxStr, pub c_ty: BoxStr, pub is_alias: bool, } @@ -553,6 +617,7 @@ pub(crate) struct TestRoundtrip { pub(crate) struct TestForeignFn { pub test_name: BoxStr, pub c_val: BoxStr, + pub rust_ty: BoxStr, pub id: BoxStr, } @@ -562,6 +627,7 @@ pub(crate) struct TestForeignStatic { pub id: BoxStr, pub c_val: BoxStr, pub rust_ty: BoxStr, + pub rust_val: BoxStr, } fn signededness_test_ident(ident: &str) -> BoxStr { @@ -620,67 +686,70 @@ impl<'a> TranslateHelper<'a> { helper } - /// Skips entire items such as structs, constants, and aliases from being tested. + /// Skips entire items such as structs, constants, and aliases from being + /// tested. /// - /// Does not skip specific tests or specific fields. If `skip_private` is true, - /// it will skip tests for all private items. + /// Does not skip specific tests or specific fields. If `skip_private` is + /// `true`, it will skip tests for all private items. fn filter_ffi_items(&mut self) { - let verbose = self.generator.verbose_skip; - - let skipped = self.filtered_ffi_items.aliases.extract_if(.., |alias| { - self.generator - .skips - .iter() - .any(|f| f(&MapInput::CEnumType(alias.ident()))) - }); - - for item in skipped { - if verbose { - eprintln!("Skipping C enum type {}", item.ident()); + fn skipper(items: &mut FfiItems, generator: &TestGenerator) { + let skipped = items.aliases.extract_if(.., |alias| { + generator + .skips + .iter() + .any(|f| f(&MapInput::CEnumType(alias.path()))) + }); + + for item in skipped { + if generator.verbose_skip { + eprintln!("Skipping C enum type {}", item.path()); + } } - } - let skipped = self - .filtered_ffi_items - .constants - .extract_if(.., |constant| { - self.generator.skips.iter().any(|f| { + let skipped = items.constants.extract_if(.., |constant| { + generator.skips.iter().any(|f| { f(&MapInput::CEnumType( &constant.ty.to_token_stream().to_string(), )) }) }); - for item in skipped { - if verbose { - eprintln!("Skipping C enum constant {}", item.ident()); + for item in skipped { + if generator.verbose_skip { + eprintln!("Skipping C enum constant {}", item.path()); + } } - } - macro_rules! filter { - ($field:ident, $variant:ident, $label:literal) => {{ - let skipped = self.filtered_ffi_items.$field.extract_if(.., |item| { - (self.generator.skip_private && !item.public) - || self - .generator - .skips - .iter() - .any(|f| f(&MapInput::$variant(item))) - }); - for item in skipped { - if verbose { - eprintln!("Skipping {} \"{}\"", $label, item.ident()) + macro_rules! filter { + ($field:ident, $variant:ident, $label:literal) => {{ + let skipped = items.$field.extract_if(.., |item| { + (generator.skip_private && !item.public) + || generator.skips.iter().any(|f| f(&MapInput::$variant(item))) + }); + for item in skipped { + if generator.verbose_skip { + eprintln!("Skipping {} \"{}\"", $label, item.path()) + } } - } - }}; + }}; + } + + filter!(aliases, Alias, "alias"); + filter!(constants, Const, "const"); + filter!(structs, Struct, "struct"); + filter!(unions, Union, "union"); + filter!(foreign_functions, Fn, "fn"); + filter!(foreign_statics, Static, "static"); + filter!(modules, Module, "module"); + + // [NOTE]: after dropping the modules that should be skipped from + // `items`, we can safely iterate through whichever ones remain. + for module in &mut items.modules { + skipper(&mut module.items, generator); + } } - filter!(aliases, Alias, "alias"); - filter!(constants, Const, "const"); - filter!(structs, Struct, "struct"); - filter!(unions, Union, "union"); - filter!(foreign_functions, Fn, "fn"); - filter!(foreign_statics, Static, "static"); + skipper(&mut self.filtered_ffi_items, self.generator); } /// Returns the equivalent C/Cpp identifier of the Rust item. @@ -692,31 +761,46 @@ impl<'a> TranslateHelper<'a> { pub(crate) fn c_type(&self, item: impl Into>) -> Result { let item: MapInput = item.into(); - let (ident, ty) = match item { - MapInput::Const(c) => (c.ident(), self.translator.translate_type(&c.ty)?), + // [NOTE]: we fetch the whole item path here instead of the identifier + // through `ident`, because `item_path` gets used only for error + // reporting. + let (item_path, ty) = match item { + MapInput::Const(c) => (c.path(), self.translator.translate_type(&c.ty)?), MapInput::StructField(_, f) => (f.ident(), self.translator.translate_type(&f.ty)?), MapInput::UnionField(_, f) => (f.ident(), self.translator.translate_type(&f.ty)?), - MapInput::Static(s) => (s.ident(), self.translator.translate_type(&s.ty)?), - // For functions, their type would be a bare fn signature, which would need to be saved - // inside of `Fn` when parsed. + MapInput::Static(s) => (s.path(), self.translator.translate_type(&s.ty)?), + // For functions, their type would be a bare fn signature, which + // would need to be saved inside of `Fn` when parsed. MapInput::Fn(_) => unimplemented!(), - // For structs/unions/aliases, their type is the same as their identifier. - MapInput::Alias(a) => (a.ident(), cdecl::named(a.ident(), Constness::Mut)), - MapInput::Struct(s) => (s.ident(), cdecl::named(s.ident(), Constness::Mut)), - MapInput::Union(u) => (u.ident(), cdecl::named(u.ident(), Constness::Mut)), - - MapInput::StructType(_) => panic!("MapInput::StructType is not allowed!"), - MapInput::UnionType(_) => panic!("MapInput::UnionType is not allowed!"), - MapInput::CEnumType(_) => panic!("MapInput::CEnumType is not allowed!"), - MapInput::StructFieldType(_, _) => panic!("MapInput::StructFieldType is not allowed!"), - MapInput::UnionFieldType(_, _) => panic!("MapInput::UnionFieldType is not allowed!"), + // For structs/unions/aliases, their type is the same as their + // identifier. + MapInput::Alias(a) => (a.path(), cdecl::named(&a.ident(), Constness::Mut)), + MapInput::Struct(s) => (s.path(), cdecl::named(&s.ident(), Constness::Mut)), + MapInput::Union(u) => (u.path(), cdecl::named(&u.ident(), Constness::Mut)), + + MapInput::StructType(_) => { + panic!("MapInput::StructType is not allowed!") + } + MapInput::UnionType(_) => { + panic!("MapInput::UnionType is not allowed!") + } + MapInput::CEnumType(_) => { + panic!("MapInput::CEnumType is not allowed!") + } + MapInput::StructFieldType(_, _) => { + panic!("MapInput::StructFieldType is not allowed!") + } + MapInput::UnionFieldType(_, _) => { + panic!("MapInput::UnionFieldType is not allowed!") + } MapInput::Type(_) => panic!("MapInput::Type is not allowed!"), + MapInput::Module(_) => panic!("MapInput::Module is not allowed!"), }; let ty = cdecl::cdecl(&ty, "".to_string()).map_err(|_| { TranslationError::new( TranslationErrorKind::InvalidReturn, - ident, + item_path, Span::call_site(), ) })?; @@ -726,3 +810,18 @@ impl<'a> TranslateHelper<'a> { Ok(self.generator.rty_to_cty(item)) } } + +#[test] +fn tmp() { + use syn::visit::Visit; + + let file = + r#"mod t { mod r { extern "C" { fn ctime() -> c_int; fn something() -> c_int; } } }"#; + let mut items = FfiItems::new(); + let file = syn::parse_file(file).unwrap(); + items.visit_file(&file); + let mut generator = TestGenerator::new(); + generator.skip_fn(|it| it.path() == "t::r::ctime"); + let translator = TranslateHelper::new(&items, &generator); + println!("{:#?}", translator.filtered_ffi_items); +} diff --git a/ctest/src/tests.rs b/ctest/src/tests.rs index baaa7ee1bdd83..ce07d696e556c 100644 --- a/ctest/src/tests.rs +++ b/ctest/src/tests.rs @@ -68,12 +68,24 @@ fn test_extraction_ffi_items() { let mut ffi_items = FfiItems::new(); ffi_items.visit_file(&ast); - assert_eq!(collect_idents!(ffi_items.aliases()), ["Foo"]); - assert_eq!(collect_idents!(ffi_items.constants()), ["bar"]); + assert!(collect_idents!(ffi_items.aliases()).is_empty()); + assert_eq!( + collect_idents!(ffi_items.modules.first().unwrap().items.aliases()), + ["Foo"] + ); + assert!(collect_idents!(ffi_items.constants()).is_empty()); + assert_eq!( + collect_idents!(ffi_items.modules.first().unwrap().items.constants()), + ["bar"] + ); assert_eq!(collect_idents!(ffi_items.foreign_functions()), ["malloc"]); assert_eq!(collect_idents!(ffi_items.foreign_statics()), ["baz"]); assert_eq!(collect_idents!(ffi_items.structs()), ["Array"]); - assert_eq!(collect_idents!(ffi_items.unions()), ["Word"]); + assert!(collect_idents!(ffi_items.unions()).is_empty()); + assert_eq!( + collect_idents!(ffi_items.modules.first().unwrap().items.unions()), + ["Word"] + ); } #[test] diff --git a/ctest/src/translator.rs b/ctest/src/translator.rs index 9f14fe65154d3..c6a9b133dc057 100644 --- a/ctest/src/translator.rs +++ b/ctest/src/translator.rs @@ -67,17 +67,20 @@ pub(crate) enum TranslationErrorKind { #[error("unsupported type")] UnsupportedType, - /// A reference to a non-primitive type was encountered, which is not supported. + /// A reference to a non-primitive type was encountered, which is not + /// supported. #[error("references to non-primitive types are not allowed")] NonPrimitiveReference, - /// Lifetimes were found in the type or function signature, which are not supported. + /// Lifetimes were found in the type or function signature, which are not + /// supported. #[error("lifetimes cannot be translated")] HasLifetimes, /// A type that is not ffi compatible was found. #[error( - "this type is not guaranteed to have a C compatible layout. See improper_ctypes_definitions lint" + "this type is not guaranteed to have a C compatible layout. See \ + improper_ctypes_definitions lint" )] NotFfiCompatible, @@ -217,7 +220,8 @@ impl<'a> Translator<'a> { if let syn::PathArguments::AngleBracketed(args) = &last.arguments && let syn::GenericArgument::Type(inner_ty) = args.args.first().unwrap() { - // Option is ONLY ffi-safe if it contains a function pointer, or a reference. + // Option is ONLY ffi-safe if it contains a function pointer, or + // a reference. match inner_ty { syn::Type::Reference(_) | syn::Type::FnPtr(_) => { return self.translate_type(inner_ty); @@ -260,13 +264,16 @@ impl<'a> Translator<'a> { /// Determine whether a C type is a signed type. /// - /// For primitive types it checks against a known list of signed types, but for aliases - /// which are the only thing other than primitives that can be signed, it recursively checks - /// the underlying type of the alias. + /// For primitive types it checks against a known list of signed types, but + /// for aliases which are the only thing other than primitives that can be + /// signed, it recursively checks the underlying type of the alias. pub(crate) fn is_signed(&self, ty: &syn::Type) -> bool { match ty { - syn::Type::Path(path) => { - let ident = path.path.segments.last().unwrap().ident.clone(); + syn::Type::Path(syn::TypePath { + path: syn::Path { segments, .. }, + .. + }) => { + let ident = segments.last().unwrap().ident.clone(); if let Some(aliased) = self.ffi_items.aliases().iter().find(|a| ident == a.ident()) { return self.is_signed(&aliased.ty); @@ -289,7 +296,20 @@ impl<'a> Translator<'a> { MapInput::StructType(name) } else if self.ffi_items.contains_union(name) { MapInput::UnionType(name) - } else if self.generator.c_enums.iter().any(|f| f(name)) { + } + // [NOTE]: for each module (which itself is a separate `FfiItems`,) we + // check first if there is some alias that corresponds with the + // passed `name`, after which we can check if the global set of + // aliases-as-C-`enum`s in `TestGenerator` (which applies to all + // modules and uses the full path to items in its routines) + // contains a remapping of the type alias (with its full path.) + else if let Some(ty) = self + .ffi_items + .aliases() + .iter() + .find(|ty| ty.ident() == name) + && self.generator.c_enums.iter().any(|f| f(ty.path())) + { MapInput::CEnumType(name) } else { MapInput::Type(name) @@ -346,9 +366,10 @@ pub(crate) fn translate_primitive_type(ty: &str) -> String { /// Construct a CTy and modify the constness of the inner type. /// -/// Basically, `syn` always gives us the `constness` of the inner type of a pointer. -/// However `cdecl::ptr` wants the `constness` of the pointer. So we just modify -/// the way it is built so that `cdecl::ptr` takes the `constness` of the inner type. +/// Basically, `syn` always gives us the `constness` of the inner type of a +/// pointer. However `cdecl::ptr` wants the `constness` of the pointer. So we +/// just modify the way it is built so that `cdecl::ptr` takes the `constness` +/// of the inner type. pub(crate) fn ptr_with_inner(inner: cdecl::CTy, constness: Constness) -> cdecl::CTy { let mut ty = Box::new(inner); match ty.deref_mut() { @@ -368,8 +389,8 @@ pub(crate) fn ptr_with_inner(inner: cdecl::CTy, constness: Constness) -> cdecl:: /// Translate a simple Rust expression to C. /// -/// This function will just pass the expression as is in most cases. In more complex cases it can -/// convert `Type as u8 + 5` to `(uint8_t)CType + 5`. +/// This function will just pass the expression as is in most cases. In more +/// complex cases it can convert `Type as u8 + 5` to `(uint8_t)CType + 5`. pub(crate) fn translate_expr(expr: &syn::Expr) -> String { match expr { syn::Expr::Index(i) => { diff --git a/ctest/templates/test.c b/ctest/templates/test.c index c9baba3dc1192..f2a706688cf95 100644 --- a/ctest/templates/test.c +++ b/ctest/templates/test.c @@ -123,7 +123,7 @@ ctest_field_ptr__{{ item.id }}__{{ item.field.ident() }}({{ item.c_ty }} *b) { #endif #ifdef __GNUC__ - // GCC emits a warning with `-Wextra` if we return a typedef to a type marked `volatile`. + // GCC emits a warning with `-Wextra` if we return a typedef to a type marked `volatile`. #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wignored-qualifiers" #endif diff --git a/ctest/templates/test.rs b/ctest/templates/test.rs index 3a71c73ba3242..697e245c842d4 100644 --- a/ctest/templates/test.rs +++ b/ctest/templates/test.rs @@ -22,6 +22,7 @@ mod generated_tests { #[allow(unused_imports)] use std::mem::{MaybeUninit, offset_of}; + #[allow(unused)] use super::*; pub static FAILED: AtomicBool = AtomicBool::new(false); @@ -30,6 +31,7 @@ mod generated_tests { /// Check that the value returned from the Rust and C side in a certain test is equivalent. /// /// Internally it will remember which checks failed and how many tests have been run. + #[allow(unused)] fn check_same(rust: T, c: T, attr: &str) { if rust != c { eprintln!("bad {attr}: rust: {rust:?} != c {c:?}"); @@ -39,6 +41,7 @@ mod generated_tests { } } + #[allow(unused)] fn check_same_bytes(rust: &[u8], c: &[u8], attr: &str) { if rust == c { NTESTS.fetch_add(1, Ordering::Relaxed); @@ -184,18 +187,18 @@ mod generated_tests { fn ctest_size_of__{{ item.id }}__{{ item.field.ident() }}() -> u64; } - let uninit_ty = MaybeUninit::<{{ item.id }}>::zeroed(); + let uninit_ty = MaybeUninit::<{{ item.rust_ty }}>::zeroed(); let uninit_ty = uninit_ty.as_ptr(); {# /* SAFETY: we assume the field access doesn't wrap */ #} - let ty_ptr = unsafe { &raw const (*uninit_ty).{{ item.field.rust_ident() }} }; + let ty_ptr = unsafe { &raw const (*uninit_ty).{{ item.field.rust_ident() }} }; {# /* SAFETY: we assume that all zeros is a valid bitpattern for `ty_ptr`, otherwise the * test should be skipped. */ #} let val = unsafe { ty_ptr.read_unaligned() }; {# /* SAFETY: FFI call with no preconditions */ #} let ctest_field_offset = unsafe { ctest_offset_of__{{ item.id }}__{{ item.field.ident() }}() }; - check_same(offset_of!({{ item.id }}, {{ item.field.rust_ident() }}) as u64, ctest_field_offset, + check_same(offset_of!({{ item.rust_ty }}, {{ item.field.rust_ident() }}) as u64, ctest_field_offset, "field offset `{{ item.field.rust_ident() }}` of `{{ item.id }}`"); {# /* SAFETY: FFI call with no preconditions */ #} let ctest_field_size = unsafe { ctest_size_of__{{ item.id }}__{{ item.field.ident() }}() }; @@ -213,7 +216,7 @@ mod generated_tests { fn ctest_field_ptr__{{ item.id }}__{{ item.field.ident() }}(a: *const {{ item.id }}) -> *mut u8; } - let uninit_ty = MaybeUninit::<{{ item.id }}>::zeroed(); + let uninit_ty = MaybeUninit::<{{ item.rust_ty }}>::zeroed(); let ty_ptr = uninit_ty.as_ptr(); // SAFETY: We don't read `field_ptr`, only compare the pointer itself. // The assumption is made that this does not wrap the address space. @@ -242,14 +245,14 @@ mod generated_tests { fn roundtrip_padding__{{ item.id }}() -> Vec { if {{ item.fields.len() }} == 0 { {# /* FIXME(ctest): What if it's an alias to a struct/union? */ #} - return vec![!{{ item.is_alias }}; size_of::<{{ item.id }}>()] + return vec![!{{ item.is_alias }}; size_of::<{{ item.rust_ty }}>()] } {# /* If there are no fields, v and bar become unused. */ #} #[allow(unused_mut)] let mut v = Vec::<(usize, usize)>::new(); #[allow(unused_variables)] - let bar = MaybeUninit::<{{ item.id }}>::zeroed(); + let bar = MaybeUninit::<{{ item.rust_ty }}>::zeroed(); #[allow(unused_variables)] let bar = bar.as_ptr(); {%- for field in item.fields +%} @@ -258,7 +261,7 @@ mod generated_tests { let val = unsafe { ty_ptr.read_unaligned() }; let size = size_of_val(&val); - let off = offset_of!({{ item.id }}, {{ field.rust_ident() }}); + let off = offset_of!({{ item.rust_ty }}, {{ field.rust_ident() }}); v.push((off, size)); {%- endfor +%} {# /* This vector contains `true` if the byte is padding and `false` if the byte is not @@ -266,7 +269,7 @@ mod generated_tests { * - padding if we have fields, this means that only the fields will be checked * - no-padding if we have a type alias: if this causes problems the type alias should * be skipped */ #} - let mut is_padding_byte = vec![true; size_of::<{{ item.id }}>()]; + let mut is_padding_byte = vec![true; size_of::<{{ item.rust_ty }}>()]; for (off, size) in &v { for i in 0..*size { is_padding_byte[off + i] = false; @@ -280,7 +283,7 @@ mod generated_tests { * It checks if the size is the same as well as if the padding bytes are all in the * correct place. For this test to be sound, `T` must be valid for any bitpattern. */ #} pub fn {{ item.test_name }}() { - type U = {{ item.id }}; + type U = {{ item.rust_ty }}; {{ ctest_extern }} "C" { fn ctest_size_of__{{ item.id }}() -> u64; fn ctest_roundtrip__{{ item.id }}( @@ -320,7 +323,7 @@ mod generated_tests { return; } - let mut c_value_bytes = vec![0; size_of::<{{ item.id }}>()]; + let mut c_value_bytes = vec![0; size_of::<{{ item.rust_ty }}>()]; let r: U = unsafe { ctest_roundtrip__{{ item.id }}(input, is_padding_byte.as_ptr(), c_value_bytes.as_mut_ptr()) }; @@ -359,7 +362,7 @@ mod generated_tests { fn ctest_foreign_fn__{{ item.id }}() -> unsafe extern "C" fn(); } let actual = unsafe { ctest_foreign_fn__{{ item.id }}() } as u64; - let expected = {{ item.id }} as *const () as u64; + let expected = {{ item.rust_ty }} as *const () as u64; check_same(actual, expected, "`{{ item.id }}` function pointer"); } {%- endfor +%} @@ -371,7 +374,7 @@ mod generated_tests { {{ ctest_extern }} "C" { fn ctest_static__{{ static_.id }}() -> *const {{ static_.rust_ty }}; } - let actual = (&raw const {{ static_.id }}).addr(); + let actual = (&raw const {{ static_.rust_val }}).addr(); let expected = unsafe { ctest_static__{{ static_.id }}().addr() }; diff --git a/ctest/tests/basic.rs b/ctest/tests/basic.rs index 593af94ad13a5..1fb3c75d797ff 100644 --- a/ctest/tests/basic.rs +++ b/ctest/tests/basic.rs @@ -111,13 +111,13 @@ fn test_skip_simple() { let library_path = "simple.out.with-skips.a"; let (mut gen_, out_dir) = default_generator(1, Some("simple.h")).unwrap(); - gen_.skip_const(|c| c.ident() == "B" || c.ident() == "A") + gen_.skip_const(|c| c.path() == "B" || c.path() == "A") .skip_c_enum(|e| e == "Color") - .skip_alias(|a| a.ident() == "Byte" || a.ident() == "gregset_t") - .skip_struct(|s| s.ident() == "Person") - .skip_union(|u| u.ident() == "Word") - .skip_fn(|f| f.ident() == "calloc") - .skip_static(|s| s.ident() == "byte"); + .skip_alias(|a| a.path() == "Byte" || a.path() == "gregset_t") + .skip_struct(|s| s.path() == "Person") + .skip_union(|u| u.path() == "Word") + .skip_fn(|f| f.path() == "calloc") + .skip_static(|s| s.path() == "byte"); check_entrypoint(&mut gen_, out_dir, crate_path, library_path, include_path); } @@ -130,7 +130,7 @@ fn test_map_simple() { let library_path = "simple.out.with-renames.a"; let (mut gen_, out_dir) = default_generator(1, Some("simple.h")).unwrap(); - gen_.rename_constant(|c| (c.ident() == "B").then(|| "C_B".to_string())) + gen_.rename_constant(|c| (c.path() == "B").then(|| "C_B".to_string())) .alias_is_c_enum(|e| e == "Color") .skip_signededness(|ty| ty == "Color"); diff --git a/ctest/tests/input/hierarchy.out.c b/ctest/tests/input/hierarchy.out.c index d34e157ee8f72..48fccdb7eaf41 100644 --- a/ctest/tests/input/hierarchy.out.c +++ b/ctest/tests/input/hierarchy.out.c @@ -29,18 +29,9 @@ typedef void (*ctest_void_func)(void); * This will later be called on the Rust side via FFI. */ -static bool ctest_const_ON_val_static = ON; - -CTEST_EXTERN bool *ctest_const__ON(void) { - return &ctest_const_ON_val_static; -} - /* Query the size and alignment of all types */ -CTEST_EXTERN uint64_t ctest_size_of__in6_addr(void) { return sizeof(in6_addr); } -CTEST_EXTERN uint64_t ctest_align_of__in6_addr(void) { return CTEST_ALIGNOF(in6_addr); } - /* Query the signedness of a type. * @@ -48,11 +39,6 @@ CTEST_EXTERN uint64_t ctest_align_of__in6_addr(void) { return CTEST_ALIGNOF(in6_ * Casting -1 to the aliased type if signed evaluates to `-1 < 0`, if unsigned to `MAX_VALUE < 0` */ -CTEST_EXTERN uint32_t ctest_signededness_of__in6_addr(void) { - in6_addr all_ones = (in6_addr) -1; - return all_ones < 0; -} - /* Query the offsets of fields and their sizes. */ @@ -66,7 +52,7 @@ CTEST_EXTERN uint32_t ctest_signededness_of__in6_addr(void) { #endif #ifdef __GNUC__ - // GCC emits a warning with `-Wextra` if we return a typedef to a type marked `volatile`. + // GCC emits a warning with `-Wextra` if we return a typedef to a type marked `volatile`. #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wignored-qualifiers" #endif @@ -79,27 +65,6 @@ CTEST_EXTERN uint32_t ctest_signededness_of__in6_addr(void) { * It checks if the size is the same as well as if the padding bytes are all in the correct place. */ -CTEST_EXTERN in6_addr ctest_roundtrip__in6_addr( - in6_addr value, - const uint8_t is_padding_byte[sizeof(in6_addr)], - uint8_t value_bytes[sizeof(in6_addr)] -) { - int size = (int)sizeof(in6_addr); - - volatile uint8_t* p = (volatile uint8_t*)&value; - int i = 0; - for (i = 0; i < size; ++i) { - - if (is_padding_byte[i]) { continue; } - value_bytes[i] = p[i]; - - uint8_t d = (uint8_t)(255) - (uint8_t)(i % 256); - d = d == 0 ? 42: d; - p[i] = d; - } - return value; -} - #ifdef __GNUC__ // Pop allow for `-Wignored-qualifiers` #pragma GCC diagnostic pop @@ -118,10 +83,6 @@ CTEST_EXTERN in6_addr ctest_roundtrip__in6_addr( /* Query a function's pointer */ -CTEST_EXTERN ctest_void_func ctest_foreign_fn__malloc(void) { - return (ctest_void_func)malloc; -} - #ifdef _MSC_VER // Pop allow for 4191 #pragma warning(default:4191) @@ -129,8 +90,3 @@ CTEST_EXTERN ctest_void_func ctest_foreign_fn__malloc(void) { /* Query pointers to statics */ - -CTEST_EXTERN void *ctest_static__in6addr_any(void) { - - return (void *)&in6addr_any; -} diff --git a/ctest/tests/input/hierarchy.out.rs b/ctest/tests/input/hierarchy.out.rs index 9a9ab6151488a..2ca2e1f2d3d33 100644 --- a/ctest/tests/input/hierarchy.out.rs +++ b/ctest/tests/input/hierarchy.out.rs @@ -18,6 +18,7 @@ mod generated_tests { #[allow(unused_imports)] use std::mem::{MaybeUninit, offset_of}; + #[allow(unused)] use super::*; pub static FAILED: AtomicBool = AtomicBool::new(false); @@ -26,6 +27,7 @@ mod generated_tests { /// Check that the value returned from the Rust and C side in a certain test is equivalent. /// /// Internally it will remember which checks failed and how many tests have been run. + #[allow(unused)] fn check_same(rust: T, c: T, attr: &str) { if rust != c { eprintln!("bad {attr}: rust: {rust:?} != c {c:?}"); @@ -35,6 +37,7 @@ mod generated_tests { } } + #[allow(unused)] fn check_same_bytes(rust: &[u8], c: &[u8], attr: &str) { if rust == c { NTESTS.fetch_add(1, Ordering::Relaxed); @@ -79,46 +82,9 @@ mod generated_tests { * This performs a byte by byte comparison of the constant value. */ - pub fn ctest_const_ON() { - type T = bool; - extern "C" { - fn ctest_const__ON() -> *const T; - } - - - - let r_val: T = ON; - let r_bytes = unsafe { - slice::from_raw_parts(ptr::from_ref(&r_val).cast::(), size_of::()) - }; - - let c_bytes = unsafe { - let c_ptr: *const T = ctest_const__ON(); - slice::from_raw_parts(c_ptr.cast::(), size_of::()) - }; - - check_same_bytes(r_bytes, c_bytes, "`ON` value"); - } - /* Compare the size and alignment of the type in Rust and C, making sure they are the same. */ - pub fn ctest_size_align_in6_addr() { - extern "C" { - fn ctest_size_of__in6_addr() -> u64; - fn ctest_align_of__in6_addr() -> u64; - } - - let rust_size = size_of::() as u64; - let c_size = unsafe { ctest_size_of__in6_addr() }; - - let rust_align = align_of::() as u64; - let c_align = unsafe { ctest_align_of__in6_addr() }; - - check_same(rust_size, c_size, "`in6_addr` size"); - check_same(rust_align, c_align, "`in6_addr` align"); - } - /* Make sure that the signededness of a type alias in Rust and C is the same. * @@ -127,17 +93,6 @@ mod generated_tests { * smaller than 0. */ - pub fn ctest_signededness_in6_addr() { - extern "C" { - fn ctest_signededness_of__in6_addr() -> u32; - } - let all_ones = !(0 as in6_addr); - let all_zeros = 0 as in6_addr; - let c_is_signed = unsafe { ctest_signededness_of__in6_addr() }; - - check_same((all_ones < all_zeros) as u32, c_is_signed, "`in6_addr` signed"); - } - /* Make sure that the offset and size of a field in a struct/union is the same. */ @@ -155,121 +110,9 @@ mod generated_tests { * go through each field and figure out the padding. */ - fn roundtrip_padding__in6_addr() -> Vec { - if 0 == 0 { - - return vec![!true; size_of::()] - } - - - #[allow(unused_mut)] - let mut v = Vec::<(usize, usize)>::new(); - #[allow(unused_variables)] - let bar = MaybeUninit::::zeroed(); - #[allow(unused_variables)] - let bar = bar.as_ptr(); - - let mut is_padding_byte = vec![true; size_of::()]; - for (off, size) in &v { - for i in 0..*size { - is_padding_byte[off + i] = false; - } - } - is_padding_byte - } - - - pub fn ctest_roundtrip_in6_addr() { - type U = in6_addr; - extern "C" { - fn ctest_size_of__in6_addr() -> u64; - fn ctest_roundtrip__in6_addr( - input: MaybeUninit, is_padding_byte: *const bool, value_bytes: *mut u8 - ) -> U; - } - - const SIZE: usize = size_of::(); - - let is_padding_byte = roundtrip_padding__in6_addr(); - let mut expected = vec![0u8; SIZE]; - let mut input = MaybeUninit::::zeroed(); - - let input_ptr = input.as_mut_ptr().cast::(); - - - for i in 0..SIZE { - let c: u8 = (i % 256) as u8; - let c = if c == 0 { 42 } else { c }; - let d: u8 = 255_u8 - (i % 256) as u8; - let d = if d == 0 { 42 } else { d }; - unsafe { - input_ptr.add(i).write_volatile(c); - expected[i] = d; - } - } - - let c_size = unsafe { ctest_size_of__in6_addr() } as usize; - if SIZE != c_size { - FAILED.store(true, Ordering::Relaxed); - eprintln!( - "size of `in6_addr` is {c_size} in C and {SIZE} in Rust\n", - ); - return; - } - - let mut c_value_bytes = vec![0; size_of::()]; - let r: U = unsafe { - ctest_roundtrip__in6_addr(input, is_padding_byte.as_ptr(), c_value_bytes.as_mut_ptr()) - }; - - - for (i, is_padding_byte) in is_padding_byte.iter().enumerate() { - if *is_padding_byte { continue; } - let rust = unsafe { *input_ptr.add(i) }; - let c = c_value_bytes[i]; - if rust != c { - eprintln!("rust[{}] = {} != {} (C): Rust `in6_addr` -> C", i, rust, c); - FAILED.store(true, Ordering::Relaxed); - } - } - - - for (i, is_padding_byte) in is_padding_byte.iter().enumerate() { - if *is_padding_byte { continue; } - let rust = expected[i] as usize; - let c = unsafe { (&raw const r).cast::().add(i).read_volatile() as usize }; - if rust != c { - eprintln!( - "rust [{i}] = {rust} != {c} (C): C `in6_addr` -> Rust", - ); - FAILED.store(true, Ordering::Relaxed); - } - } - } - /* Check if the Rust and C side function pointers point to the same underlying function. */ - pub fn ctest_foreign_fn_malloc() { - extern "C" { - fn ctest_foreign_fn__malloc() -> unsafe extern "C" fn(); - } - let actual = unsafe { ctest_foreign_fn__malloc() } as u64; - let expected = malloc as *const () as u64; - check_same(actual, expected, "`malloc` function pointer"); - } - /* Tests if the pointer to the static variable matches in both Rust and C. */ - - pub fn ctest_static_in6addr_any() { - extern "C" { - fn ctest_static__in6addr_any() -> *const in6_addr; - } - let actual = (&raw const in6addr_any).addr(); - let expected = unsafe { - ctest_static__in6addr_any().addr() - }; - check_same(actual, expected, "`in6addr_any` static"); - } } use generated_tests::*; @@ -291,10 +134,4 @@ fn main() { // FIXME(ctest): Maybe consider running the tests in parallel, since everything is independent // and we already use atomics. fn run_all() { - ctest_const_ON(); - ctest_size_align_in6_addr(); - ctest_signededness_in6_addr(); - ctest_roundtrip_in6_addr(); - ctest_foreign_fn_malloc(); - ctest_static_in6addr_any(); } diff --git a/ctest/tests/input/macro.out.c b/ctest/tests/input/macro.out.c index 3fe5cf1d91b14..d170831c1ebe3 100644 --- a/ctest/tests/input/macro.out.c +++ b/ctest/tests/input/macro.out.c @@ -141,7 +141,7 @@ ctest_field_ptr__VecU16__y(struct VecU16 *b) { #endif #ifdef __GNUC__ - // GCC emits a warning with `-Wextra` if we return a typedef to a type marked `volatile`. + // GCC emits a warning with `-Wextra` if we return a typedef to a type marked `volatile`. #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wignored-qualifiers" #endif diff --git a/ctest/tests/input/macro.out.edition-2024.c b/ctest/tests/input/macro.out.edition-2024.c index 3fe5cf1d91b14..d170831c1ebe3 100644 --- a/ctest/tests/input/macro.out.edition-2024.c +++ b/ctest/tests/input/macro.out.edition-2024.c @@ -141,7 +141,7 @@ ctest_field_ptr__VecU16__y(struct VecU16 *b) { #endif #ifdef __GNUC__ - // GCC emits a warning with `-Wextra` if we return a typedef to a type marked `volatile`. + // GCC emits a warning with `-Wextra` if we return a typedef to a type marked `volatile`. #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wignored-qualifiers" #endif diff --git a/ctest/tests/input/macro.out.edition-2024.rs b/ctest/tests/input/macro.out.edition-2024.rs index 6970695f02ff0..fe0ee081a42dd 100644 --- a/ctest/tests/input/macro.out.edition-2024.rs +++ b/ctest/tests/input/macro.out.edition-2024.rs @@ -18,6 +18,7 @@ mod generated_tests { #[allow(unused_imports)] use std::mem::{MaybeUninit, offset_of}; + #[allow(unused)] use super::*; pub static FAILED: AtomicBool = AtomicBool::new(false); @@ -26,6 +27,7 @@ mod generated_tests { /// Check that the value returned from the Rust and C side in a certain test is equivalent. /// /// Internally it will remember which checks failed and how many tests have been run. + #[allow(unused)] fn check_same(rust: T, c: T, attr: &str) { if rust != c { eprintln!("bad {attr}: rust: {rust:?} != c {c:?}"); @@ -35,6 +37,7 @@ mod generated_tests { } } + #[allow(unused)] fn check_same_bytes(rust: &[u8], c: &[u8], attr: &str) { if rust == c { NTESTS.fetch_add(1, Ordering::Relaxed); @@ -135,7 +138,7 @@ mod generated_tests { let uninit_ty = uninit_ty.as_ptr(); - let ty_ptr = unsafe { &raw const (*uninit_ty).x }; + let ty_ptr = unsafe { &raw const (*uninit_ty).x }; let val = unsafe { ty_ptr.read_unaligned() }; @@ -159,7 +162,7 @@ mod generated_tests { let uninit_ty = uninit_ty.as_ptr(); - let ty_ptr = unsafe { &raw const (*uninit_ty).y }; + let ty_ptr = unsafe { &raw const (*uninit_ty).y }; let val = unsafe { ty_ptr.read_unaligned() }; @@ -183,7 +186,7 @@ mod generated_tests { let uninit_ty = uninit_ty.as_ptr(); - let ty_ptr = unsafe { &raw const (*uninit_ty).x }; + let ty_ptr = unsafe { &raw const (*uninit_ty).x }; let val = unsafe { ty_ptr.read_unaligned() }; @@ -207,7 +210,7 @@ mod generated_tests { let uninit_ty = uninit_ty.as_ptr(); - let ty_ptr = unsafe { &raw const (*uninit_ty).y }; + let ty_ptr = unsafe { &raw const (*uninit_ty).y }; let val = unsafe { ty_ptr.read_unaligned() }; diff --git a/ctest/tests/input/macro.out.rs b/ctest/tests/input/macro.out.rs index 25555e0c25650..d129f26f45a96 100644 --- a/ctest/tests/input/macro.out.rs +++ b/ctest/tests/input/macro.out.rs @@ -18,6 +18,7 @@ mod generated_tests { #[allow(unused_imports)] use std::mem::{MaybeUninit, offset_of}; + #[allow(unused)] use super::*; pub static FAILED: AtomicBool = AtomicBool::new(false); @@ -26,6 +27,7 @@ mod generated_tests { /// Check that the value returned from the Rust and C side in a certain test is equivalent. /// /// Internally it will remember which checks failed and how many tests have been run. + #[allow(unused)] fn check_same(rust: T, c: T, attr: &str) { if rust != c { eprintln!("bad {attr}: rust: {rust:?} != c {c:?}"); @@ -35,6 +37,7 @@ mod generated_tests { } } + #[allow(unused)] fn check_same_bytes(rust: &[u8], c: &[u8], attr: &str) { if rust == c { NTESTS.fetch_add(1, Ordering::Relaxed); @@ -135,7 +138,7 @@ mod generated_tests { let uninit_ty = uninit_ty.as_ptr(); - let ty_ptr = unsafe { &raw const (*uninit_ty).x }; + let ty_ptr = unsafe { &raw const (*uninit_ty).x }; let val = unsafe { ty_ptr.read_unaligned() }; @@ -159,7 +162,7 @@ mod generated_tests { let uninit_ty = uninit_ty.as_ptr(); - let ty_ptr = unsafe { &raw const (*uninit_ty).y }; + let ty_ptr = unsafe { &raw const (*uninit_ty).y }; let val = unsafe { ty_ptr.read_unaligned() }; @@ -183,7 +186,7 @@ mod generated_tests { let uninit_ty = uninit_ty.as_ptr(); - let ty_ptr = unsafe { &raw const (*uninit_ty).x }; + let ty_ptr = unsafe { &raw const (*uninit_ty).x }; let val = unsafe { ty_ptr.read_unaligned() }; @@ -207,7 +210,7 @@ mod generated_tests { let uninit_ty = uninit_ty.as_ptr(); - let ty_ptr = unsafe { &raw const (*uninit_ty).y }; + let ty_ptr = unsafe { &raw const (*uninit_ty).y }; let val = unsafe { ty_ptr.read_unaligned() }; diff --git a/ctest/tests/input/simple.out.with-renames.c b/ctest/tests/input/simple.out.with-renames.c index 23f0b45bca6b9..86ac54601fd0c 100644 --- a/ctest/tests/input/simple.out.with-renames.c +++ b/ctest/tests/input/simple.out.with-renames.c @@ -230,7 +230,7 @@ ctest_field_ptr__Word__byte(union Word *b) { #endif #ifdef __GNUC__ - // GCC emits a warning with `-Wextra` if we return a typedef to a type marked `volatile`. + // GCC emits a warning with `-Wextra` if we return a typedef to a type marked `volatile`. #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wignored-qualifiers" #endif diff --git a/ctest/tests/input/simple.out.with-renames.rs b/ctest/tests/input/simple.out.with-renames.rs index 1736e9941e13b..f332ddaac3adb 100644 --- a/ctest/tests/input/simple.out.with-renames.rs +++ b/ctest/tests/input/simple.out.with-renames.rs @@ -18,6 +18,7 @@ mod generated_tests { #[allow(unused_imports)] use std::mem::{MaybeUninit, offset_of}; + #[allow(unused)] use super::*; pub static FAILED: AtomicBool = AtomicBool::new(false); @@ -26,6 +27,7 @@ mod generated_tests { /// Check that the value returned from the Rust and C side in a certain test is equivalent. /// /// Internally it will remember which checks failed and how many tests have been run. + #[allow(unused)] fn check_same(rust: T, c: T, attr: &str) { if rust != c { eprintln!("bad {attr}: rust: {rust:?} != c {c:?}"); @@ -35,6 +37,7 @@ mod generated_tests { } } + #[allow(unused)] fn check_same_bytes(rust: &[u8], c: &[u8], attr: &str) { if rust == c { NTESTS.fetch_add(1, Ordering::Relaxed); @@ -326,7 +329,7 @@ mod generated_tests { let uninit_ty = uninit_ty.as_ptr(); - let ty_ptr = unsafe { &raw const (*uninit_ty).name }; + let ty_ptr = unsafe { &raw const (*uninit_ty).name }; let val = unsafe { ty_ptr.read_unaligned() }; @@ -350,7 +353,7 @@ mod generated_tests { let uninit_ty = uninit_ty.as_ptr(); - let ty_ptr = unsafe { &raw const (*uninit_ty).age }; + let ty_ptr = unsafe { &raw const (*uninit_ty).age }; let val = unsafe { ty_ptr.read_unaligned() }; @@ -374,7 +377,7 @@ mod generated_tests { let uninit_ty = uninit_ty.as_ptr(); - let ty_ptr = unsafe { &raw const (*uninit_ty).job }; + let ty_ptr = unsafe { &raw const (*uninit_ty).job }; let val = unsafe { ty_ptr.read_unaligned() }; @@ -398,7 +401,7 @@ mod generated_tests { let uninit_ty = uninit_ty.as_ptr(); - let ty_ptr = unsafe { &raw const (*uninit_ty).favorite_color }; + let ty_ptr = unsafe { &raw const (*uninit_ty).favorite_color }; let val = unsafe { ty_ptr.read_unaligned() }; @@ -422,7 +425,7 @@ mod generated_tests { let uninit_ty = uninit_ty.as_ptr(); - let ty_ptr = unsafe { &raw const (*uninit_ty).word }; + let ty_ptr = unsafe { &raw const (*uninit_ty).word }; let val = unsafe { ty_ptr.read_unaligned() }; @@ -446,7 +449,7 @@ mod generated_tests { let uninit_ty = uninit_ty.as_ptr(); - let ty_ptr = unsafe { &raw const (*uninit_ty).byte }; + let ty_ptr = unsafe { &raw const (*uninit_ty).byte }; let val = unsafe { ty_ptr.read_unaligned() }; diff --git a/ctest/tests/input/simple.out.with-skips.c b/ctest/tests/input/simple.out.with-skips.c index bf2ce3cba863c..e9c2167269185 100644 --- a/ctest/tests/input/simple.out.with-skips.c +++ b/ctest/tests/input/simple.out.with-skips.c @@ -60,7 +60,7 @@ CTEST_EXTERN uint32_t ctest_signededness_of__volatile_char(void) { #endif #ifdef __GNUC__ - // GCC emits a warning with `-Wextra` if we return a typedef to a type marked `volatile`. + // GCC emits a warning with `-Wextra` if we return a typedef to a type marked `volatile`. #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wignored-qualifiers" #endif diff --git a/ctest/tests/input/simple.out.with-skips.rs b/ctest/tests/input/simple.out.with-skips.rs index 125e8a564fcf0..71b7508911d12 100644 --- a/ctest/tests/input/simple.out.with-skips.rs +++ b/ctest/tests/input/simple.out.with-skips.rs @@ -18,6 +18,7 @@ mod generated_tests { #[allow(unused_imports)] use std::mem::{MaybeUninit, offset_of}; + #[allow(unused)] use super::*; pub static FAILED: AtomicBool = AtomicBool::new(false); @@ -26,6 +27,7 @@ mod generated_tests { /// Check that the value returned from the Rust and C side in a certain test is equivalent. /// /// Internally it will remember which checks failed and how many tests have been run. + #[allow(unused)] fn check_same(rust: T, c: T, attr: &str) { if rust != c { eprintln!("bad {attr}: rust: {rust:?} != c {c:?}"); @@ -35,6 +37,7 @@ mod generated_tests { } } + #[allow(unused)] fn check_same_bytes(rust: &[u8], c: &[u8], attr: &str) { if rust == c { NTESTS.fetch_add(1, Ordering::Relaxed); diff --git a/libc-test/build/main.rs b/libc-test/build/main.rs index c6b1a271e5221..22939d7558cf0 100755 --- a/libc-test/build/main.rs +++ b/libc-test/build/main.rs @@ -87,13 +87,13 @@ fn ctest_cfg() -> ctest::TestGenerator { cfg.skip_private(true); // Skip anonymous unions/structs. - cfg.skip_union(|u| u.ident().starts_with("__c_anonymous_")); - cfg.skip_struct(|s| s.ident().starts_with("__c_anonymous_")); - cfg.skip_alias(|ty| ty.ident().starts_with("__c_anonymous_")); + cfg.skip_union(|u| u.path().starts_with("__c_anonymous_")); + cfg.skip_struct(|s| s.path().starts_with("__c_anonymous_")); + cfg.skip_alias(|ty| ty.path().starts_with("__c_anonymous_")); // __uint128 is not declared in C, but is an alias we export. // FIXME(1.0): These aliases will eventually be removed. - cfg.skip_alias(|ty| ty.ident() == "__uint128"); + cfg.skip_alias(|ty| ty.path() == "__uint128"); if env::var("LIBC_CI_ZBUILD_STD").is_ok() { *cfg.expansion_cargo_args_mut() = vec!["-Zbuild-std=core,std".into()]; @@ -266,7 +266,7 @@ fn test_apple(t: &Target) { ); cfg.skip_struct(move |s| { - match s.ident() { + match s.path() { // Extern types "DIR" | "FILE" | "fpos_t" | "timezone" | "_opaque_pthread_t" => true, @@ -286,7 +286,7 @@ fn test_apple(t: &Target) { }); cfg.skip_const(move |constant| { - match constant.ident() { + match constant.path() { // FIXME(deprecated): Removed since 12.0.1 / xnu-8019.41.5. See `ttycom.h` at // https://github.com/apple-oss-distributions/xnu/commit/e6231be02a03711ca404e5121a151b24afbff733 "TIOCREMOTE" => true, @@ -306,7 +306,7 @@ fn test_apple(t: &Target) { }); cfg.skip_alias(move |ty| { - match ty.ident() { + match ty.path() { // FIXME(macos): The size is changed in macOS/iOS/... 27. "vm_statistics64_data_t" => apple.unwrap() < (27, 0), _ => false, @@ -315,7 +315,7 @@ fn test_apple(t: &Target) { cfg.skip_fn(move |func| { // skip those that are manually verified - match func.ident() { + match func.path() { // close calls the close_nocancel system call on x86 "close" if x86_64 => true, // FIXME(1.0): std removed libresolv support: https://github.com/rust-lang/rust/pull/102766 @@ -329,7 +329,7 @@ fn test_apple(t: &Target) { }); cfg.skip_struct_field(move |struct_, field| { - match (struct_.ident(), field.ident()) { + match (struct_.path(), field.ident()) { // Anonymous ADT fields ("ifreq", "ifr_ifru") => true, ("in6_ifreq", "ifr_ifru") => true, @@ -341,10 +341,10 @@ fn test_apple(t: &Target) { cfg.skip_struct_field_type(move |struct_, field| { // The type of `bfl_u` is an anonymous union - (struct_.ident(), field.ident()) == ("bpf_dltlist", "bfl_u") + (struct_.path(), field.ident()) == ("bpf_dltlist", "bfl_u") }); - cfg.volatile_struct_field(|s, f| s.ident() == "aiocb" && f.ident() == "aio_buf"); + cfg.volatile_struct_field(|s, f| s.path() == "aiocb" && f.ident() == "aio_buf"); cfg.rename_struct_ty(move |ty| { // Just pass all these through, no need for a "struct" prefix @@ -361,13 +361,13 @@ fn test_apple(t: &Target) { cfg.rename_struct_field(|s, f| { match f.ident() { - n if n.ends_with("_nsec") && s.ident().starts_with("stat") => { + n if n.ends_with("_nsec") && s.path().starts_with("stat") => { Some(n.replace("e_nsec", "espec.tv_nsec")) } // FIXME(macos): sigaction actually contains a union with two variants: // a sa_sigaction with type: (*)(int, struct __siginfo *, void *) // a sa_handler with type sig_t - "sa_sigaction" if s.ident() == "sigaction" => Some("sa_handler".to_string()), + "sa_sigaction" if s.path() == "sigaction" => Some("sa_handler".to_string()), _ => None, } }); @@ -494,7 +494,7 @@ fn test_openbsd(t: &Target) { }); cfg.rename_struct_field(|struct_, field_| { - let struct_ = struct_.ident(); + let struct_ = struct_.path(); let replacement = match field_.ident() { "st_birthtime" if struct_.starts_with("stat") => "__st_birthtime".to_string(), "st_birthtime_nsec" if struct_.starts_with("stat") => "__st_birthtimensec".to_string(), @@ -517,7 +517,7 @@ fn test_openbsd(t: &Target) { }); cfg.skip_const(move |constant| { - match constant.ident() { + match constant.path() { // Removed in OpenBSD 7.7 (unused since 1991) "ATF_COM" | "ATF_PERM" | "ATF_PUBL" | "ATF_USETRAILERS" => true, @@ -537,7 +537,7 @@ fn test_openbsd(t: &Target) { }); cfg.skip_struct(move |struct_| { - match struct_.ident() { + match struct_.path() { // Extern types "DIR" | "FILE" | "fpos_t" | "sem" | "timezone" => true, @@ -546,7 +546,7 @@ fn test_openbsd(t: &Target) { }); cfg.skip_struct_field(move |struct_, field| { - match (struct_.ident(), field.ident()) { + match (struct_.path(), field.ident()) { // conflicting with `p_type` macro from . ("Elf32_Phdr", "p_type") => true, ("Elf64_Phdr", "p_type") => true, @@ -641,7 +641,7 @@ fn test_cygwin(t: &Target) { }); cfg.skip_const(move |constant| { - match constant.ident() { + match constant.path() { // FIXME(cygwin): these constants do not exist on Cygwin "ARPOP_REQUEST" | "ARPOP_REPLY" | "ATF_COM" | "ATF_PERM" | "ATF_PUBL" | "ATF_USETRAILERS" => true, @@ -668,7 +668,7 @@ fn test_cygwin(t: &Target) { }); cfg.skip_struct(move |struct_| { - match struct_.ident() { + match struct_.path() { // Extern types "DIR" | "FILE" | "fpos_t" => true, @@ -680,21 +680,21 @@ fn test_cygwin(t: &Target) { match field.ident() { // Our stat *_nsec fields normally don't actually exist but are part // of a timeval struct - s if s.ends_with("_nsec") && struct_.ident().starts_with("stat") => { + s if s.ends_with("_nsec") && struct_.path().starts_with("stat") => { Some(s.replace("e_nsec", ".tv_nsec")) } // FIXME(cygwin): sigaction actually contains a union with two variants: // a sa_sigaction with type: (*)(int, struct __siginfo *, void *) // a sa_handler with type sig_t - "sa_sigaction" if struct_.ident() == "sigaction" => Some("sa_handler".to_string()), + "sa_sigaction" if struct_.path() == "sigaction" => Some("sa_handler".to_string()), _ => None, } }); cfg.skip_struct_field(|struct_, field| { - match (struct_.ident(), field.ident()) { + match (struct_.path(), field.ident()) { // this is actually a union on linux, so we can't represent it well and // just insert some padding. ("ifreq", "ifr_ifru") => true, @@ -706,7 +706,7 @@ fn test_cygwin(t: &Target) { cfg.skip_fn(move |func| { // skip those that are manually verified - match func.ident() { + match func.path() { // There are two versions of the sterror_r function, see // // https://linux.die.net/man/3/strerror_r @@ -813,10 +813,10 @@ fn test_windows(t: &Target) { cfg.rename_fn(move |func| { func.link_name() .map(|l| l.to_string()) - .or(func.ident().to_string().into()) + .or(func.path().to_string().into()) }); - cfg.skip_alias(move |alias| match alias.ident() { + cfg.skip_alias(move |alias| match alias.path() { "SSIZE_T" if !gnu => true, "ssize_t" if !gnu => true, // FIXME(windows): The size and alignment of this type are incorrect @@ -825,7 +825,7 @@ fn test_windows(t: &Target) { }); cfg.skip_struct(move |struct_| { - match struct_.ident() { + match struct_.path() { // FIXME(windows): The size and alignment of this struct are incorrect "timespec" if gnu && x86_32 => true, // Extern types @@ -835,7 +835,7 @@ fn test_windows(t: &Target) { }); cfg.skip_const(move |constant| { - match constant.ident() { + match constant.path() { // FIXME(windows): API error: // SIG_ERR type is "void (*)(int)", not "int" "SIG_ERR" | @@ -847,7 +847,7 @@ fn test_windows(t: &Target) { } }); - cfg.skip_struct_field(move |s, field| s.ident() == "CONTEXT" && field.ident() == "Fp"); + cfg.skip_struct_field(move |s, field| s.path() == "CONTEXT" && field.ident() == "Fp"); // FIXME(windows): All functions point to the wrong addresses? cfg.skip_fn_ptrcheck(|_| true); @@ -1029,7 +1029,7 @@ fn test_solarish(t: &Target) { headers!(cfg, "sys/lgrp_user_impl.h",); } - cfg.skip_alias(move |ty| match ty.ident() { + cfg.skip_alias(move |ty| match ty.path() { "sighandler_t" => true, _ => false, }); @@ -1047,7 +1047,7 @@ fn test_solarish(t: &Target) { }); cfg.rename_struct_field(move |struct_, field| { - match struct_.ident() { + match struct_.path() { // rust struct was committed with typo for Solaris "door_arg_t" if field.ident() == "dec_num" => Some("desc_num".to_string()), "stat" if field.ident().ends_with("_nsec") => { @@ -1058,7 +1058,7 @@ fn test_solarish(t: &Target) { } }); - cfg.skip_const(move |constant| match constant.ident() { + cfg.skip_const(move |constant| match constant.path() { "DT_FIFO" | "DT_CHR" | "DT_DIR" | "DT_BLK" | "DT_REG" | "DT_LNK" | "DT_SOCK" | "USRQUOTA" | "GRPQUOTA" | "PRIO_MIN" | "PRIO_MAX" => true, @@ -1095,13 +1095,13 @@ fn test_solarish(t: &Target) { cfg.skip_union(|union_| { // the union handling is a mess - if union_.ident().contains("door_desc_t_") { + if union_.path().contains("door_desc_t_") { return true; } false }); cfg.skip_struct(move |struct_| { - match struct_.ident() { + match struct_.path() { // the union handling is a mess x if x.contains("door_desc_t_") => true, @@ -1117,11 +1117,11 @@ fn test_solarish(t: &Target) { cfg.skip_struct_field_type(move |struct_, field| { // aio_buf is "volatile void*" - struct_.ident() == "aiocb" && field.ident() == "aio_buf" + struct_.path() == "aiocb" && field.ident() == "aio_buf" }); cfg.skip_struct_field(move |s, field| { - match (s.ident(), field.ident()) { + match (s.path(), field.ident()) { // C99 sizing on this is tough ("dirent", "d_name") => true, // the union/macro makes this rough @@ -1150,7 +1150,7 @@ fn test_solarish(t: &Target) { cfg.skip_fn(move |func| { // skip those that are manually verified - match func.ident() { + match func.path() { // const-ness only added recently "dladdr" => true, @@ -1342,7 +1342,7 @@ fn test_netbsd(t: &Target) { match field.ident() { // Our stat *_nsec fields normally don't actually exist but are part // of a timeval struct - s if s.ends_with("_nsec") && struct_.ident().starts_with("stat") => { + s if s.ends_with("_nsec") && struct_.path().starts_with("stat") => { Some(s.replace("e_nsec", ".tv_nsec")) } _ => None, @@ -1352,7 +1352,7 @@ fn test_netbsd(t: &Target) { cfg.alias_is_c_enum(|ty| ty == "fae_action"); cfg.skip_alias(move |ty| { - match ty.ident() { + match ty.path() { // FIXME(netbsd): sighandler_t is crazy across platforms "sighandler_t" => true, // Incomplete type in C @@ -1363,7 +1363,7 @@ fn test_netbsd(t: &Target) { }); cfg.skip_struct(move |struct_| { - match struct_.ident() { + match struct_.path() { // These are tested as part of the linux_fcntl tests since there are // header conflicts when including them with all the other structs. "termios2" => true, @@ -1392,7 +1392,7 @@ fn test_netbsd(t: &Target) { }); cfg.skip_const(move |constant| { - match constant.ident() { + match constant.path() { "SIG_DFL" | "SIG_ERR" | "SIG_IGN" => true, // sighandler_t weirdness // deprecated, obsolete upstream @@ -1414,7 +1414,7 @@ fn test_netbsd(t: &Target) { cfg.skip_fn(move |func| { #[expect(clippy::wildcard_in_or_patterns)] - match func.ident() { + match func.path() { // FIXME(netbsd): Look into setting `_POSIX_C_SOURCE` to enable this "qsort_r" => true, @@ -1434,7 +1434,7 @@ fn test_netbsd(t: &Target) { }); cfg.skip_struct_field_type(move |struct_, field| { - match (struct_.ident(), field.ident()) { + match (struct_.path(), field.ident()) { // This is a weird union, don't check the type. ("ifaddrs", "ifa_ifu") => true, // sighandler_t type is super weird @@ -1446,7 +1446,7 @@ fn test_netbsd(t: &Target) { }); cfg.skip_struct_field(|struct_, field| { - match (struct_.ident(), field.ident()) { + match (struct_.path(), field.ident()) { // conflicting with `p_type` macro from . ("Elf32_Phdr", "p_type") => true, ("Elf64_Phdr", "p_type") => true, @@ -1471,7 +1471,7 @@ fn test_netbsd(t: &Target) { // Unless otherwise noted, everything in this block was an addition in NetBS 10. if netbsd9 { - cfg.skip_const(move |constant| match constant.ident() { + cfg.skip_const(move |constant| match constant.path() { "EOWNERDEAD" | "ENOTRECOVERABLE" | "F_GETPATH" @@ -1492,7 +1492,7 @@ fn test_netbsd(t: &Target) { _ => false, }); - cfg.skip_struct(move |struct_| match struct_.ident() { + cfg.skip_struct(move |struct_| match struct_.path() { "sockaddr_dl" if !netbsd9 => true, // Last field increased size in 10 x if x.starts_with("ptrace_lwp") => true, // These were packed before NetBSD 10 @@ -1500,13 +1500,13 @@ fn test_netbsd(t: &Target) { _ => false, }); - cfg.skip_fn(move |func| match func.ident() { + cfg.skip_fn(move |func| match func.path() { "reallocarray" | "getentropy" | "ppoll" | "getrandom" => true, x if x.starts_with("timerfd_") => true, _ => false, }); - cfg.skip_struct_field(|struct_, field| match (struct_.ident(), field.ident()) { + cfg.skip_struct_field(|struct_, field| match (struct_.path(), field.ident()) { ("statvfs", "f_mntfromlabel") => true, // added field ("kevent", "udata") => true, // changed type (ABI-compatible) _ => false, @@ -1652,18 +1652,18 @@ fn test_dragonflybsd(t: &Target) { match field.ident() { // Our stat *_nsec fields normally don't actually exist but are part // of a timeval struct - s if s.ends_with("_nsec") && struct_.ident().starts_with("stat") => { + s if s.ends_with("_nsec") && struct_.path().starts_with("stat") => { Some(s.replace("e_nsec", ".tv_nsec")) } // Field is named `type` in C but that is a Rust keyword, // so these fields are translated to `type_` in the bindings. - "type_" if struct_.ident() == "rtprio" => Some("type".to_string()), + "type_" if struct_.path() == "rtprio" => Some("type".to_string()), _ => None, } }); cfg.skip_alias(move |ty| { - match ty.ident() { + match ty.path() { // sighandler_t is crazy across platforms "sighandler_t" => true, _ => false, @@ -1671,7 +1671,7 @@ fn test_dragonflybsd(t: &Target) { }); cfg.skip_struct(move |struct_| { - match struct_.ident() { + match struct_.path() { // FIXME(dragonflybsd): These are tested as part of the linux_fcntl tests since // there are header conflicts when including them with all the other // structs. @@ -1703,7 +1703,7 @@ fn test_dragonflybsd(t: &Target) { }); cfg.skip_const(move |constant| { - match constant.ident() { + match constant.path() { "SIG_DFL" | "SIG_ERR" | "SIG_IGN" => true, // sighandler_t weirdness // Kernel-only symbols in DragonFly headers. @@ -1763,7 +1763,7 @@ fn test_dragonflybsd(t: &Target) { cfg.skip_fn(move |func| { // skip those that are manually verified - match func.ident() { + match func.path() { "getrlimit" | "getrlimit64" | // non-int in 1st arg "setrlimit" | "setrlimit64" | // non-int in 1st arg "prlimit" | "prlimit64" // non-int in 2nd arg @@ -1783,7 +1783,7 @@ fn test_dragonflybsd(t: &Target) { }); cfg.skip_alias(move |ty| { - match ty.ident() { + match ty.path() { // sighandler_t is crazy across platforms "sighandler_t" => true, // Same as FreeBSD: `kvm_t` is an opaque handle used through @@ -1794,7 +1794,7 @@ fn test_dragonflybsd(t: &Target) { }); cfg.skip_struct(move |struct_| { - match struct_.ident() { + match struct_.path() { // FIXME(dragonflybsd): These are tested as part of the linux_fcntl tests since // there are header conflicts when including them with all the other // structs. @@ -1807,7 +1807,7 @@ fn test_dragonflybsd(t: &Target) { }); cfg.skip_struct_field(move |struct_, field| { - match (struct_.ident(), field.ident()) { + match (struct_.path(), field.ident()) { // this is actually a union on linux, so we can't represent it well and // just insert some padding. ("siginfo_t", "_pad") => true, @@ -1825,7 +1825,7 @@ fn test_dragonflybsd(t: &Target) { }); cfg.skip_struct_field_type(move |struct_, field| { - match (struct_.ident(), field.ident()) { + match (struct_.path(), field.ident()) { // This is a weird union, don't check the type. ("ifaddrs", "ifa_ifu") => true, // sighandler_t type is super weird @@ -1937,18 +1937,18 @@ fn test_wasi(t: &Target) { // These have a different and internal type in header files and are only // used here to generate a pointer to them in bindings so skip these tests. - cfg.skip_static(|s| s.ident().starts_with("_CLOCK_")); + cfg.skip_static(|s| s.path().starts_with("_CLOCK_")); match wasi_sdk.1 { WasiVersion::P1 => {} // This was removed in wasip2 target for wasi-sdk-30+, but it's just a // typedef, so ignore it. _ => { - cfg.skip_alias(|s| s.ident() == "__wasi_rights_t"); + cfg.skip_alias(|s| s.path() == "__wasi_rights_t"); } } - cfg.skip_const(|c| match c.ident() { + cfg.skip_const(|c| match c.path() { // These constants aren't yet defined in wasi-libc. // Exposing them is being tracked by https://github.com/WebAssembly/wasi-libc/issues/531. "SO_BROADCAST" | "SO_LINGER" => true, @@ -1957,14 +1957,14 @@ fn test_wasi(t: &Target) { }); cfg.skip_struct(move |struct_| { - match struct_.ident() { + match struct_.path() { // Extern types "DIR" | "FILE" | "__locale_struct" => true, _ => false, } }); - cfg.skip_fn(|f| match f.ident() { + cfg.skip_fn(|f| match f.path() { // This function doesn't actually exist in libc's header files "__errno_location" => true, @@ -1979,7 +1979,7 @@ fn test_wasi(t: &Target) { // d_name is declared as a flexible array in WASI libc, so it // doesn't support sizeof. - cfg.skip_struct_field(|s, field| s.ident() == "dirent" && field.ident() == "d_name"); + cfg.skip_struct_field(|s, field| s.path() == "dirent" && field.ident() == "d_name"); ctest::generate_test(&mut cfg, "../src/lib.rs", "ctest_output.rs").unwrap(); } @@ -2162,7 +2162,7 @@ fn test_android(t: &Target) { }); cfg.rename_struct_field(move |struct_, field| { - match (struct_.ident(), field.ident()) { + match (struct_.path(), field.ident()) { // Our stat *_nsec fields normally don't actually exist but are part // of a timeval struct ("stat" | "statfs" | "statvfs" | "stat64" | "statfs64" | "statvfs64", f) @@ -2179,7 +2179,7 @@ fn test_android(t: &Target) { }); cfg.skip_alias(move |ty| { - match ty.ident() { + match ty.path() { // FIXME(android): `sighandler_t` type is incorrect, see: // https://github.com/rust-lang/libc/issues/1359 "sighandler_t" => true, @@ -2198,7 +2198,7 @@ fn test_android(t: &Target) { }); cfg.skip_struct(move |struct_| { - match struct_.ident() { + match struct_.path() { // These are tested as part of the linux_fcntl tests since there are // header conflicts when including them with all the other structs. "termios2" => true, @@ -2224,7 +2224,7 @@ fn test_android(t: &Target) { }); cfg.skip_const(move |constant| { - match constant.ident() { + match constant.path() { // The IPV6 constants are tested in the `linux_ipv6.rs` tests: | "IPV6_FLOWINFO" | "IPV6_FLOWLABEL_MGR" @@ -2365,7 +2365,7 @@ fn test_android(t: &Target) { cfg.skip_fn(move |func| { // skip those that are manually verified - match func.ident() { + match func.path() { // FIXME(android): for unknown reasons linker unable to find "fexecve" "fexecve" => true, @@ -2446,7 +2446,7 @@ fn test_android(t: &Target) { }); cfg.skip_struct_field_type(move |struct_, field| { - match (struct_.ident(), field.ident()) { + match (struct_.path(), field.ident()) { // This is a weird union, don't check the type. ("ifaddrs", "ifa_ifu") => true, // this one is an anonymous union @@ -2463,7 +2463,7 @@ fn test_android(t: &Target) { }); cfg.skip_struct_field(|struct_, field| { - match (struct_.ident(), field.ident()) { + match (struct_.path(), field.ident()) { // conflicting with `p_type` macro from . ("Elf32_Phdr", "p_type") => true, ("Elf64_Phdr", "p_type") => true, @@ -2688,7 +2688,7 @@ fn test_freebsd(t: &Target) { }); cfg.rename_struct_field(|struct_, field_| { - let struct_ = struct_.ident(); + let struct_ = struct_.path(); let replacement = match field_.ident() { // Our stat *_nsec fields normally don't actually exist but are part // of a timeval struct @@ -2709,7 +2709,7 @@ fn test_freebsd(t: &Target) { }); cfg.skip_const(move |constant| { - match constant.ident() { + match constant.path() { // These constants were introduced in FreeBSD 13: "F_ADD_SEALS" | "F_GET_SEALS" | "F_SEAL_SEAL" | "F_SEAL_SHRINK" | "F_SEAL_GROW" | "F_SEAL_WRITE" @@ -2982,7 +2982,7 @@ fn test_freebsd(t: &Target) { }); cfg.skip_alias(move |ty| { - match ty.ident() { + match ty.path() { // the struct "__kvm" is quite tricky to bind so since we only use a pointer to it // for now, it doesn't matter too much... "kvm_t" => true, @@ -2994,7 +2994,7 @@ fn test_freebsd(t: &Target) { }); cfg.skip_struct(move |struct_| { - match struct_.ident() { + match struct_.path() { // `procstat` is a private struct "procstat" => true, @@ -3040,7 +3040,7 @@ fn test_freebsd(t: &Target) { cfg.skip_fn(move |func| { // skip those that are manually verified - match func.ident() { + match func.path() { // This is introduced in FreeBSD 14.1 "execvpe" => true, @@ -3102,10 +3102,10 @@ fn test_freebsd(t: &Target) { // aio_buf is a volatile void* but since we cannot express that in // Rust types, we have to explicitly tell the checker about it here: - cfg.volatile_struct_field(|s, f| s.ident() == "aiocb" && f.ident() == "aio_buf"); + cfg.volatile_struct_field(|s, f| s.path() == "aiocb" && f.ident() == "aio_buf"); cfg.skip_struct_field(move |struct_, field| { - match (struct_.ident(), field.ident()) { + match (struct_.path(), field.ident()) { // FIXME(freebsd): `sa_sigaction` has type `sighandler_t` but that type is // incorrect, see: https://github.com/rust-lang/libc/issues/1359 ("sigaction", "sa_sigaction") => true, @@ -3290,7 +3290,7 @@ fn test_emscripten(t: &Target) { match field.ident() { // Our stat *_nsec fields normally don't actually exist but are part // of a timeval struct - s if s.ends_with("_nsec") && struct_.ident().starts_with("stat") => { + s if s.ends_with("_nsec") && struct_.path().starts_with("stat") => { Some(s.replace("e_nsec", ".tv_nsec")) } _ => None, @@ -3298,7 +3298,7 @@ fn test_emscripten(t: &Target) { }); cfg.skip_alias(move |ty| { - match ty.ident() { + match ty.path() { // sighandler_t is crazy across platforms // FIXME(emscripten): is this necessary? "sighandler_t" => true, @@ -3310,7 +3310,7 @@ fn test_emscripten(t: &Target) { }); cfg.skip_union(move |union_| { - match union_.ident() { + match union_.path() { // FIXME(emscripten): Investigate why the test fails. // Skip for now to unblock CI. "sigval" => true, @@ -3324,7 +3324,7 @@ fn test_emscripten(t: &Target) { }); cfg.skip_alias(|ty| { - match ty.ident() { + match ty.path() { // LFS64 types have been removed in Emscripten 3.1.44 // https://github.com/emscripten-core/emscripten/pull/19812 ty => ty.ends_with("64") || ty.ends_with("64_t"), @@ -3332,7 +3332,7 @@ fn test_emscripten(t: &Target) { }); cfg.skip_struct(move |struct_| { - match struct_.ident() { + match struct_.path() { // FIXME(emscripten): Investigate why the test fails. // Skip for now to unblock CI. "pthread_condattr_t" => true, @@ -3351,7 +3351,7 @@ fn test_emscripten(t: &Target) { }); cfg.skip_fn(move |func| { - match func.ident() { + match func.path() { // Emscripten does not support fork/exec/wait or any kind of multi-process support // https://github.com/emscripten-core/emscripten/blob/3.1.68/tools/system_libs.py#L1100 "execv" | "execve" | "execvp" | "execvpe" | "fexecve" | "wait4" => true, @@ -3372,7 +3372,7 @@ fn test_emscripten(t: &Target) { }); cfg.skip_const(move |constant| { - match constant.ident() { + match constant.path() { // FIXME(emscripten): emscripten uses different constants to constructs these n if n.contains("__SIZEOF_PTHREAD") => true, @@ -3416,7 +3416,7 @@ fn test_emscripten(t: &Target) { }); cfg.skip_struct_field_type(move |struct_, field| { - match (struct_.ident(), field.ident()) { + match (struct_.path(), field.ident()) { // This is a weird union, don't check the type. ("ifaddrs", "ifa_ifu") => true, // sighandler_t type is super weird @@ -3426,7 +3426,7 @@ fn test_emscripten(t: &Target) { }); cfg.skip_struct_field(move |struct_, field| { - match (struct_.ident(), field.ident()) { + match (struct_.path(), field.ident()) { // _sigev_un is an anonymous union ("sigevent", "_sigev_un") => true, // this is actually a union on linux, so we can't represent it well and @@ -3611,7 +3611,7 @@ fn test_neutrino(t: &Target) { _ => None, }); - cfg.volatile_struct_field(|s, f| match (s.ident(), f.ident()) { + cfg.volatile_struct_field(|s, f| match (s.path(), f.ident()) { ("aiocb", "aio_buf") => true, ("qtime_entry", "nsec_tod_adjust") => true, ("qtime_entry", "nsec") => true, @@ -3621,7 +3621,7 @@ fn test_neutrino(t: &Target) { }); cfg.skip_alias(move |ty| { - match ty.ident() { + match ty.path() { // FIXME(sighandler): `sighandler_t` type is incorrect, see: // https://github.com/rust-lang/libc/issues/1359 "sighandler_t" => true, @@ -3634,7 +3634,7 @@ fn test_neutrino(t: &Target) { }); cfg.skip_struct(move |struct_| { - match struct_.ident() { + match struct_.path() { "Elf64_Phdr" | "Elf32_Phdr" => true, // union @@ -3648,7 +3648,7 @@ fn test_neutrino(t: &Target) { }); cfg.skip_const(move |constant| { - match constant.ident() { + match constant.path() { // These signal "functions" are actually integer values that are casted to a fn ptr // This causes the compiler to err because of "illegal cast of int to ptr". "SIG_DFL" => true, @@ -3670,7 +3670,7 @@ fn test_neutrino(t: &Target) { cfg.skip_fn(move |func| { // skip those that are manually verified - match func.ident() { + match func.path() { // wrong signature "signal" => true, @@ -3704,12 +3704,12 @@ fn test_neutrino(t: &Target) { cfg.skip_struct_field_type(move |struct_, field| { // Anonymous structures - struct_.ident() == "_idle_hook" && field.ident() == "time" + struct_.path() == "_idle_hook" && field.ident() == "time" }); cfg.skip_struct_field(|struct_, field| { matches!( - (struct_.ident(), field.ident()), + (struct_.path(), field.ident()), ("__sched_param", "reserved") | ("sched_param", "reserved") | ("sigevent", "__padding1") // ensure alignment @@ -3719,7 +3719,7 @@ fn test_neutrino(t: &Target) { ) }); - cfg.skip_static(move |static_| static_.ident() == "__dso_handle"); + cfg.skip_static(move |static_| static_.path() == "__dso_handle"); ctest::generate_test(&mut cfg, "../src/lib.rs", "ctest_output.rs").unwrap(); } @@ -3819,7 +3819,7 @@ fn test_vxworks(t: &Target) { "net/if.h", ); // FIXME(vxworks) - cfg.skip_const(move |constant| match constant.ident() { + cfg.skip_const(move |constant| match constant.path() { // sighandler_t weirdness "SIG_DFL" | "SIG_ERR" | "SIG_IGN" // These are not defined in VxWorks @@ -3831,13 +3831,13 @@ fn test_vxworks(t: &Target) { _ => false, }); // FIXME(vxworks) - cfg.skip_alias(move |ty| match ty.ident() { + cfg.skip_alias(move |ty| match ty.path() { "stat64" | "sighandler_t" | "off64_t" => true, _ => false, }); cfg.skip_struct_field_type( - move |struct_, field| match (struct_.ident(), field.ident()) { + move |struct_, field| match (struct_.path(), field.ident()) { ("siginfo_t", "si_value") | ("stat", "st_size") // sighandler_t type is super weird @@ -3857,7 +3857,7 @@ fn test_vxworks(t: &Target) { }); // FIXME(vxworks) - cfg.skip_fn(move |func| match func.ident() { + cfg.skip_fn(move |func| match func.path() { // sighandler_t "signal" // This is used a realpath and not _realpath @@ -3868,7 +3868,7 @@ fn test_vxworks(t: &Target) { }); // Not defined in vxworks. Just a crate specific union type. - cfg.skip_union(move |u| u.ident() == "sa_u_t"); + cfg.skip_union(move |u| u.path() == "sa_u_t"); ctest::generate_test(&mut cfg, "../src/lib.rs", "ctest_output.rs").unwrap(); } @@ -4261,7 +4261,7 @@ fn test_linux(t: &Target) { }); cfg.rename_struct_field(move |struct_, field| { - match (struct_.ident(), field.ident()) { + match (struct_.path(), field.ident()) { // Our stat *_nsec fields normally don't actually exist but are part // of a timeval struct - this is fixed in musl_v1_2 ("stat" | "statfs" | "statvfs" | "stat64" | "statfs64" | "statvfs64", f) @@ -4283,7 +4283,7 @@ fn test_linux(t: &Target) { }); cfg.skip_alias(move |alias| { - let ty = alias.ident(); + let ty = alias.path(); // FIXME(musl): very recent additions to musl, not yet released. // also apparently some glibc versions if ty == "Elf32_Relr" || ty == "Elf64_Relr" { @@ -4328,7 +4328,7 @@ fn test_linux(t: &Target) { }); cfg.skip_struct(move |struct_| { - let ty = struct_.ident(); + let ty = struct_.path(); // LFS64 types have been removed in musl 1.2.4+ if musl && (ty.ends_with("64") || ty.ends_with("64_t")) { @@ -4470,7 +4470,7 @@ fn test_linux(t: &Target) { }); cfg.skip_const(move |constant| { - let name = constant.ident(); + let name = constant.path(); // FIXME(linux): Requires newer kernel headers than CI has. These uapi/linux/mount.h // constants (OPEN_TREE_NAMESPACE landed in v7.0, FSCONFIG_CMD_CREATE_EXCL in v6.6) @@ -4895,7 +4895,7 @@ fn test_linux(t: &Target) { }); cfg.skip_fn(move |function| { - let name = function.ident(); + let name = function.path(); // skip those that are manually verified match name { // There are two versions of the sterror_r function, see @@ -5006,7 +5006,7 @@ fn test_linux(t: &Target) { }); cfg.skip_struct_field_type(move |union_, field| { - match (union_.ident(), field.ident()) { + match (union_.path(), field.ident()) { // This is a weird union, don't check the type. ("ifaddrs", "ifa_ifu") => true, // sighandler_t type is super weird @@ -5035,10 +5035,10 @@ fn test_linux(t: &Target) { } }); - cfg.volatile_struct_field(|s, f| s.ident() == "aiocb" && f.ident() == "aio_buf"); + cfg.volatile_struct_field(|s, f| s.path() == "aiocb" && f.ident() == "aio_buf"); cfg.skip_struct_field(move |struct_, field| { - match (struct_.ident(), field.ident()) { + match (struct_.path(), field.path()) { // musl names this __dummy1 but it's still there ("glob_t", "gl_flags") if musl => true, // musl seems to define this as an *anonymous* bitfield @@ -5174,10 +5174,10 @@ fn test_linux(t: &Target) { "B3500000", "B38400", "B4000000", "B460800", "B4800", "B50", "B500000", "B57600", "B576000", "B600", "B614400", "B7200", "B75", "B76800", "B921600", "B9600", ] - .contains(&s.ident()) + .contains(&s.path()) }); if mips || sparc { - cfg.skip_const(|s| s.ident() == "NCCS"); + cfg.skip_const(|s| s.path() == "NCCS"); } // old symbols, so tests fail if glibc is too new // note: `skip_fn_ptrcheck` overrides the previous function @@ -5196,7 +5196,7 @@ fn test_linux(t: &Target) { }); // old structs, so tests fail if glibc is too new if mips || sparc { - cfg.skip_struct(|s| s.ident() == "termios"); + cfg.skip_struct(|s| s.path() == "termios"); } } @@ -5229,7 +5229,7 @@ fn test_linux_like_apis(t: &Target) { .skip_const(|_| true) .skip_struct(|_| true) .skip_union(|_| true) - .skip_fn(|function| function.ident() != "strerror_r"); + .skip_fn(|function| function.path() != "strerror_r"); ctest::generate_test(&mut cfg, "../src/lib.rs", "linux_strerror_r.rs").unwrap(); } @@ -5252,7 +5252,7 @@ fn test_linux_like_apis(t: &Target) { .skip_struct(|_| true) .skip_union(|_| true) .skip_fn(|_| true) - .skip_const(move |constant| !fnctl_constants.contains(&constant.ident())); + .skip_const(move |constant| !fnctl_constants.contains(&constant.path())); config_gnu_bits(t, &mut cfg); if musl { @@ -5277,9 +5277,9 @@ fn test_linux_like_apis(t: &Target) { .skip_alias(|_| true) .skip_static(|_| true) .skip_fn(|_| true) - .skip_const(move |constant| !termios_constants.contains(&constant.ident())) + .skip_const(move |constant| !termios_constants.contains(&constant.path())) .skip_union(|_| true) - .skip_struct(|s| s.ident() != "termios2") + .skip_struct(|s| s.path() != "termios2") .rename_type(move |ty| match ty { "Ioctl" if gnu => Some("unsigned long".to_string()), "Ioctl" => Some("int".to_string()), @@ -5304,7 +5304,7 @@ fn test_linux_like_apis(t: &Target) { .skip_fn(|_| true) .skip_struct(|_| true) .skip_union(|_| true) - .skip_const(move |constant| !ipv6_constants.contains(&constant.ident())); + .skip_const(move |constant| !ipv6_constants.contains(&constant.path())); config_gnu_bits(t, &mut cfg); headers!(cfg, "linux/in6.h",); @@ -5325,8 +5325,8 @@ fn test_linux_like_apis(t: &Target) { .skip_const(|_| true) .skip_union(|_| true) .rename_struct_ty(move |ty| Some(ty.to_string())) - .skip_struct(move |struct_| !elf_structs.contains(&struct_.ident())) - .skip_alias(move |alias| !elf_structs.contains(&alias.ident())); + .skip_struct(move |struct_| !elf_structs.contains(&struct_.path())) + .skip_alias(move |alias| !elf_structs.contains(&alias.path())); config_gnu_bits(t, &mut cfg); @@ -5344,7 +5344,7 @@ fn test_linux_like_apis(t: &Target) { .skip_struct(|_| true) .skip_union(|_| true) .skip_alias(|_| true) - .skip_const(move |constant| constant.ident() != "ARPHRD_CAN"); + .skip_const(move |constant| constant.path() != "ARPHRD_CAN"); ctest::generate_test(&mut cfg, "../src/lib.rs", "linux_if_arp.rs").unwrap(); } @@ -5497,7 +5497,7 @@ fn test_haiku(t: &Target) { ); cfg.skip_struct(move |struct_| { - match struct_.ident() { + match struct_.path() { // FIXME(haiku): locale_t does not exist on Haiku "locale_t" => true, // FIXME(haiku): rusage has a different layout on Haiku @@ -5523,7 +5523,7 @@ fn test_haiku(t: &Target) { }); cfg.skip_alias(move |ty| { - match ty.ident() { + match ty.path() { // FIXME(haiku): locale_t does not exist on Haiku "locale_t" => true, // These cause errors, to be reviewed in the future @@ -5538,7 +5538,7 @@ fn test_haiku(t: &Target) { cfg.skip_fn(move |func| { // skip those that are manually verified - match func.ident() { + match func.path() { // FIXME(haiku): does not exist on haiku "open_wmemstream" => true, "mlockall" | "munlockall" => true, @@ -5562,7 +5562,7 @@ fn test_haiku(t: &Target) { }); cfg.skip_const(move |constant| { - match constant.ident() { + match constant.path() { // FIXME(haiku): these constants do not exist on Haiku "DT_UNKNOWN" | "DT_FIFO" | "DT_CHR" | "DT_DIR" | "DT_BLK" | "DT_REG" | "DT_LNK" | "DT_SOCK" => true, @@ -5588,7 +5588,7 @@ fn test_haiku(t: &Target) { }); cfg.skip_struct_field(move |struct_, field| { - match (struct_.ident(), field.ident()) { + match (struct_.path(), field.ident()) { // FIXME(time): the stat struct actually has timespec members, whereas // the current representation has these unpacked. ("stat", "st_atime") => true, @@ -5659,7 +5659,7 @@ fn test_haiku(t: &Target) { }); cfg.rename_struct_field(move |struct_, field| { - let struct_ = struct_.ident(); + let struct_ = struct_.path(); match field.ident() { // Field is named `type` in C but that is a Rust keyword, // so these fields are translated to `type_` in the bindings. @@ -5777,7 +5777,7 @@ fn test_aix(t: &Target) { "wchar.h", ); - cfg.skip_alias(move |ty| match ty.ident() { + cfg.skip_alias(move |ty| match ty.path() { // AIX does not define type 'sighandler_t'. "sighandler_t" => true, @@ -5794,7 +5794,7 @@ fn test_aix(t: &Target) { _ => None, }); - cfg.skip_const(move |constant| match constant.ident() { + cfg.skip_const(move |constant| match constant.path() { // Skip 'sighandler_t' assignments. "SIG_DFL" | "SIG_ERR" | "SIG_IGN" => true, @@ -5821,7 +5821,7 @@ fn test_aix(t: &Target) { }); cfg.skip_struct(move |struct_| { - match struct_.ident() { + match struct_.path() { // 'struct fpreg_t' is not defined in AIX headers. It is created to // allow type 'double' to be used in signal contexts. "fpreg_t" => true, @@ -5838,7 +5838,7 @@ fn test_aix(t: &Target) { }); cfg.skip_union(|union_| { - match union_.ident() { + match union_.path() { // '__poll_ctl_ext_u' and '__pollfd_ext_u' are for unnamed unions. "__poll_ctl_ext_u" => true, "__pollfd_ext_u" => true, @@ -5856,7 +5856,7 @@ fn test_aix(t: &Target) { }); cfg.skip_struct_field_type(move |struct_, field| { - match (struct_.ident(), field.ident()) { + match (struct_.path(), field.ident()) { // AIX does not define 'sighandler_t'. ("sigaction", "sa_sigaction") => true, @@ -5878,7 +5878,7 @@ fn test_aix(t: &Target) { }); cfg.skip_struct_field(move |s, field| { - match s.ident() { + match s.path() { // The field 'u' is actually a unnamed union in the AIX header. "poll_ctl_ext" if field.ident() == "u" => true, "pollfd_ext_t" if field.ident() == "u" => true, @@ -5891,7 +5891,7 @@ fn test_aix(t: &Target) { }); cfg.skip_fn(move |func| { - match func.ident() { + match func.path() { // 'sighandler_t' is not defined on AIX. "signal" => true, @@ -5954,7 +5954,7 @@ fn test_aix(t: &Target) { } }); - cfg.volatile_struct_field(|s, f| match (s.ident(), f.ident()) { + cfg.volatile_struct_field(|s, f| match (s.path(), f.ident()) { // 'aio_buf' is of type 'volatile void**' but since we cannot // express that in Rust types, we have to explicitly tell the // checker about it here. @@ -5995,7 +5995,7 @@ fn test_qurt(t: &Target) { // QuRT doesn't have all the standard unix types/structs cfg.skip_struct(|s| { - match s.ident() { + match s.path() { // These are compatibility stubs in libc, not from QuRT headers "stat" | "tm" | "timespec" | "timeval" | "itimerspec" | "dirent" | "DIR" | "termios" | "rlimit" | "rusage" | "flock" | "div_t" | "ldiv_t" | "lldiv_t" => true, @@ -6004,7 +6004,7 @@ fn test_qurt(t: &Target) { }); cfg.skip_alias(|ty| { - match ty.ident() { + match ty.path() { // Skip types not defined in QuRT POSIX headers "intptr_t" | "uintptr_t" | "ptrdiff_t" | "size_t" | "ssize_t" | "time_t" | "suseconds_t" | "useconds_t" | "timer_t" | "dev_t" | "ino_t" | "mode_t" @@ -6026,7 +6026,7 @@ fn test_qurt(t: &Target) { }); cfg.skip_const(|c| { - let name = c.ident(); + let name = c.path(); match name { // Skip constants not from QuRT POSIX headers "EOK" | "PAGESIZE" | "PAGE_SIZE" | "L_tmpnam" | "TMP_MAX" | "FOPEN_MAX" => true, @@ -6129,7 +6129,7 @@ fn test_qurt(t: &Target) { }); cfg.skip_fn(|func| { - let name = func.ident(); + let name = func.path(); match name { // Skip functions not from QuRT POSIX headers we're testing "strlen" | "strcpy" | "strncpy" | "strcat" | "strncat" | "strcmp" | "strncmp" @@ -6196,7 +6196,7 @@ fn test_qurt(t: &Target) { // Skip field checks for opaque types cfg.skip_struct_field(|s, f| { // pthread_attr_t bitfield can't be checked directly - s.ident() == "pthread_attr_t" && f.ident() == "__bitfield" + s.path() == "pthread_attr_t" && f.ident() == "__bitfield" }); cfg.skip_roundtrip(|_| true); diff --git a/notes/Demo.idr b/notes/Demo.idr new file mode 100644 index 0000000000000..3acc9b1d35319 --- /dev/null +++ b/notes/Demo.idr @@ -0,0 +1,192 @@ +module Demo + +import Data.List + +%default total + +data UseTree : Type where Path : String -> UseTree -> UseTree + Name : String -> UseTree + Glob : UseTree + Group : List UseTree -> UseTree + +-- [NOTE]: we do not currently consider item paths. We consider solely +-- identifiers. This applies to both `FfiItems` and its list of items +-- (themselves solely identifiers.) +record FfiItems where + constructor MkFfiItems + ident : String + items : List String + mods : List FfiItems + uses : List UseTree + +data Resolution : Type where Resolved : UseTree -> FfiItems -> Resolution + Unresolved : UseTree -> Resolution + +-- [NOTE]: the next two types are only used as proof witnesses and will not get +-- ported over to Rust. Instead, a new, refined type will be used in Rust to +-- express the constraints imposed by the below dependent types. + +data Ungrouped : UseTree -> Type where NameWitness : Ungrouped (Name _) + GlobWitness : Ungrouped Glob + PathWitness : {auto prf : Ungrouped t} + -> Ungrouped (Path _ t) + +data UngroupedItems : FfiItems -> Type where + EmptyWitness : UngroupedItems (MkFfiItems _ _ _ []) + Witness : {auto prfs : UngroupedItems (MkFfiItems mid is ms ts)} + -> {auto prf : Ungrouped t} + -> UngroupedItems (MkFfiItems mid is ms (t :: ts)) + +empty : String -> FfiItems +empty s = MkFfiItems s [] [] [] + +-- [NOTE]: this deconstructs a module that is proven to contain no group +-- imports, into its list of imports (also proven to not contain group imports.) +-- The inverse of this is done by the `re` function. +ex : (i : FfiItems ** UngroupedItems i) -> List (u : UseTree ** Ungrouped u) +ex ((MkFfiItems _ _ _ []) ** _) = + [] +ex a@(MkFfiItems mid is ms (h :: t) ** Witness {prfs} {prf}) = + let na = ((MkFfiItems mid is ms t) ** prfs) in + (h ** prf) :: (ex $ assert_smaller a na) + +-- [NOTE]: this builds up a proof tree by deconstructing the already proven +-- trees of group-clean imports. The goal is to explain to the type-checker +-- that these imports will make up a module that is guaranteed to be clean of +-- group imports. The inverse of this is done by the `ex` function. +re : List (u : UseTree ** Ungrouped u) + -> FfiItems + -> (i : FfiItems ** UngroupedItems i) +re [] (MkFfiItems mid is ms _) = + (MkFfiItems mid is ms [] ** EmptyWitness) +re ((h ** hw) :: t) i = + let (MkFfiItems mid is ms us ** w) = re t i in + (MkFfiItems mid is ms (h :: us) ** Witness {prfs = w} {prf = hw}) + +normalize : FfiItems -> (i : FfiItems ** UngroupedItems i) +normalize it = let nl = (foldr f []) . uses $ it in re nl it where + -- [NOTE]: this function requires asserting to the totality checker that the + -- trees rooted at group imports are always bound to be smaller than the trees + -- rooted one level above. This is because the shape of the `UseTree` type in + -- group imports stops "growing" when it finds lists. The items of these lists + -- (themselves trees) could then be found to be potentially larger than the + -- top-level parent group. This is a contradiction because a path's tail's + -- group import is always smaller than the sum of the path's tail and the + -- path's head. + f : UseTree + -> List (u : UseTree ** Ungrouped u) + -> List (u : UseTree ** Ungrouped u) + f = (++) . f' where + f' : UseTree -> List (u : UseTree ** Ungrouped u) + f' (Name id) = + [ ((Name id) ** NameWitness) ] + f' Glob = + [ (Glob ** GlobWitness) ] + f' (Path id t) = + map (\(t ** w) => (Path id t ** PathWitness {prf = w})) (f' t) + f' o@(Group l) = + map (\t => f' $ assert_smaller o t) l |> join + +resolveOne : (i : FfiItems ** UngroupedItems i) -> List Resolution +resolveOne a@(it ** _) = + join . (map $ \(u ** prf) => resolveReexport u u {prf} it) . ex $ a where + f : String -> FfiItems -> Maybe (Either String FfiItems) + f id (MkFfiItems _ is ms _) = find c ((map Left is) ++ (map Right ms)) where + c : Either String FfiItems -> Bool + c (Left s) = s == id + c (Right (MkFfiItems mid _ _ _)) = mid == id + + resolveReexport : UseTree + -> (t : UseTree) + -> {auto 0 prf : Ungrouped t} + -> FfiItems + -> List Resolution + resolveReexport oid (Name id) {prf = NameWitness} it@(MkFfiItems mid _ _ _) + = case f id it of + Just (Left i) => + [ oid |> Resolved $ { items := [ i ] } . empty $ mid ] + Just (Right m) => + [ oid |> Resolved $ { mods := [ m ] } . empty $ mid ] + Nothing => + [ Unresolved oid ] + resolveReexport oid Glob {prf = GlobWitness} it + = [ Resolved oid it ] + resolveReexport oid (Path id t) {prf = PathWitness {prf}} it + = case f id it of Just (Right m) => resolveReexport oid t {prf} m + Just _ => [ Unresolved oid ] + Nothing => [ Unresolved oid ] + +merge : (i : FfiItems ** UngroupedItems i) + -> List Resolution + -> (i : FfiItems ** UngroupedItems i) +merge it@(_ ** _) [] = it +merge it@(_ ** _) ((Unresolved _) :: t) = merge it t +merge it@(iit ** _) ((Resolved oid (MkFfiItems _ is ms _)) :: t) = merge nit t + where nit : (i : FfiItems ** UngroupedItems i) + nit = let nus = (deleteBy f oid) . ex $ it + in { items $= (++ is), mods $= (++ ms) } iit |> re nus where + f : UseTree -> (u : UseTree ** Ungrouped u) -> Bool + f (Name id1) (Name id2 ** _) = + id1 == id2 + f Glob (Glob ** _) = + True + f (Path id1 t1) (Path id2 t2 ** PathWitness {prf}) = + id1 == id2 && f t1 (t2 ** prf) + f _ _ = + False + +covering +resolve : FfiItems -> FfiItems +resolve it = let nit := normalize . { mods $= map resolve } $ it + in f nit where + e : FfiItems -> Nat + e (MkFfiItems _ _ _ us) = length us + + f : (i : FfiItems ** UngroupedItems i) -> FfiItems + f a@(it ** _) = let na@(nit ** _) := (merge a) . resolveOne $ a + in case e it == e nit of True => nit + False => f na + +-- [NOTE]: the following tests comprise only test data. To test it out, the +-- Idris REPL is required. + +-- [NOTE]: this test is for the resolution function `resolve`. +test1 : FfiItems +test1 = let bar := { items := [ "Foo" ] } . empty $ "bar" + foo := { mods := [ bar ] } . empty $ "foo" + in { mods := [ foo ] + , uses := [ Path "bar" Glob, Path "foo" Glob ] } . empty $ "" + +-- [NOTE]: this test is for the normalization function `normalize`. +test2 : FfiItems +test2 = + let g := [ Name "TypeId", Name "Any" ] + in { uses := [ Path "std" (Path "any" (Group g)) ] } . empty $ "" + +-- [NOTE]: this test is for the normalization function `normalize`. +test3 : FfiItems +test3 = + let g := [ Path "foo" (Path "bar" (Group [ Name "Ty" + , Path "test" (Name "foobar") ])) + , Name "TypeId" + , Name "Any" ] + in { uses := [ Path "std" (Path "any" (Group g)) ] } . empty $ "" + +-- [NOTE]: this test is for the resolution function `resolve`. +test4 : FfiItems +test4 = + let uses := [ Path "std" (Path "os" (Path "raw" (Name "c_void"))) + , Path "level1" Glob ] + level1 := { items := [ "Foo", "bar", "Word" ] } . empty $ "level1" + mods := [ level1 ] + items := [ "Array", "baz", "malloc" ] + in { items := items, mods := mods, uses := uses } . empty $ "" + +-- [NOTE]: this test is for the resolution function `resolve`. +test5 : FfiItems +test5 = + let foobar := { items := [ "Foo" ] } . empty $ "foobar" + bar := { mods := [ foobar ] } . empty $ "bar" + foo := { mods := [ bar ] } . empty $ "foo" + in { uses := [ Path "foobar" Glob, Path "bar" Glob, Path "foo" (Name "bar") ] + , mods := [ foo ] } . empty $ "" diff --git a/notes/main.typ b/notes/main.typ new file mode 100644 index 0000000000000..4fe769f5166ca --- /dev/null +++ b/notes/main.typ @@ -0,0 +1,309 @@ +#import "@local/scratchpad:0.1.4": * + +#show: template.with(title: [Notes on extending `ctest`]) + +#title() + +Extending `ctest` requires keeping track of the ```rust use``` statements while +parsing to get right the item visibility across modules. We could very well be +producing tests for an item without it being used in its "source" module. + +This, though, shouldn't matter much. The module from which we parsed the item +should be enough to test it; The problem comes when filtering. Folks expect to +filter items that are potentially reexported. + +Suppose somebody sets up a skip for a record `Foo`, and trusts that there will +be an item path `bar::Foo` that will match this item. The item is defined in +module `crate::bar::foo::Foo`, but reexported in module `bar`. + +While going through the skips in the `ctest` internal logic, we find that this +item will never get a hit. The path associated with it is `bar::foo::Foo`, and +not `bar::Foo`. + +Solving this particular need could go through parsing as well reexports, and +scanning them afterwards. This second pass would add "synonyms" to the item +paths of each parsed item. Skips would run against each of those paths. + +This is assuming our only needs are concerned with filtering. This is not +necessarily the case. Or maybe it is. `ctest` parses all items in a crate. Then +it generates tests that refer to items on the Rust side of things. + +There is one more usecase for ```rust use```-statement parsing. Suppose somebody +sets up a record that is not public under path `crate::foo::Bar`. Then they +reexport it under path `crate::Bar`. + +Then suppose they call ```rust TestGenerator::skip_private```. We are again +screwed big time if the Rust tests refer to the item through the private item +path. Or are we, now? How does that option work when not set? + +Apparently, I do not have to worry about item resolution. We can refer to both +public and private items in the generated tests. This means the only thing that +needs to work with reexports are filters. + +This means that function ```rust TranslateHelper::filter_ffi_items``` may be the +only thing that needs changing. That function filters out all items that will be +tested, so we can also recursively filter items there. + +The simplest approach that comes to mind to have multiple identifiers assigned +to a given symbol is to perform multiple passes. Once all symbols are parsed +into `FfiItems`, we make one full traversal per parsed item. + +Each full traversal of the same `FfiItems` ensures for each given item, we keep +all paths that could be used to refer to that item in scope. Once `Ffiitems` is +parsed, it will not change anymore until we filter out elements. + +This is not the best approach when it comes to performance, but it will do for +an initial implementation. In terms of memory storage, to avoid funny allocation +issues, we can clone `FfiItems` initially. + +The cloned `FfiItems` is then used for the passes; The original `FfiItems` gets +its parsed items modified with alternative paths. Seems fair enough. We fully +own the items within it at filter-time, so the plan seems feasible. + +The problem comes when you think about item resolution in those secondary +passes. It is non-trivial to implement because at module +```rust crate::bar::foo``` you can have the following code: + +```rust +pub struct Bar; +``` + +Then back at module ```rust crate::bar```, you can have the following code. + +```rust +mod foo; + +use foo::*; +``` + +But you could also have this: + +```rust +mod foo; + +use self::foo::*; +``` + +And for that matter, you could have instead module +```rust crate::bar::barfoo::foo```, with the following layout: + +/ Module ```rust crate::bar::barfoo::foo```: + ```rust + pub struct Bar; + ``` + +/ Module ```rust crate::bar::barfoo```: + ```rust + mod foo; + mod test; + use foo::*; + ``` + +/ Module ```rust crate::bar```: + ```rust + use foo::*; // `foo` comes from `barfoo`'s reexport below + mod barfoo; + use barfoo::*; + ``` + +The above situation is one of a number of potentially complex item resolution +scenarios that I would have to deal with. Maybe there is some library that does +this for me, or maybe I can use some library straight from `rustc`. + +It seems like `rustc_resolve` does just this, but it is not made for use outside +the compiler, so it is not available in `crates.io`. The next best thing would +be to inline each of these, and perform multiple passes per module. + +Inlining is not enitrely clear to me right now, but the multi-passes would work +a lot like Typst's convergence algorithm. One pass yields modules we know are +child modules to the current one, and modules we don't know about. + +Then we can assume all imports that have not been resolved yet need special +attention in subsequent passes. After the first pass, we have expanded the +"importable" items, so we should be capable of resolving all "delayed" imports. + +Following up from the prior example, we can say that the first pass would yield +the following information: + +```rust +// module `crate::bar` +use foo::*; // [UNRESOLVED] +mod barfoo; +use barfoo::*; // [RESOLVED] +``` + +The resulting ```rust FfiItems``` for module ```rust crate::bar``` would contain +one module, and all items from that one child module. + +That would leave us with an ```rust FfiItems``` for module ```rust crate::bar``` +consisting of two modules; Module `barfoo` and module `barfoo::foo`. But +resolving solely modules this way is useless. + +Instead, the algorithm could be something along the lines of (a bit like +Bellman-Ford except without proof of correctness:) + +/ Algorithm 1: \ + Inputs: + + - An ```rust FfiItems``` instance with the parsed contents of a full crate. + The parsed contents must contain both currently parsed items and import + statements (so just items.) + + Outputs: + + - *Pending*. + + Steps: + + + Match on the list of child modules to the input ```rust FfiItems```. + + - If there are no child modules, return unity (*Pending*.) + + - If there are any modules, extract the next module. + + + Match on the list of reexports in the extracted module. + + - If there are no reexports, proceed as follows. + + + Run algorithm 1. Set its input to be a new ```rust FfiItems``` with + its modules as the tail list of the current list of modules, barring + the extracted module. + + - If there are any reexports, extract the next reexport. + + + Run algorithm 2. Set the input import statement to be the extracted + reexport. Set the input ```rust FfiItems``` to be the current + input's ```rust FfiItems```. + +/ Algorithm 2: \ + Inputs: + + - An import ```rust use``` statement. + + - A base ```rust FfiItems``` corresponding to the module where the above + import statement lives at. + + Outputs: + + - A list of a coproduct type. The type considers two data constructors; One + for _resolved_ modules, and another for _unresolved_ modules. The former + takes a single parameter of type ```rust FfiItems```. + + This returns a list instead of a single ```rust FfiItems``` instance because + a given ```rust use``` statement could refer to a group in its tail segment. + Each element of the group could itself expand to an arbitrary reexport. + + Steps: + + + Match against the type of input import. + + - If the import is a path, proceed as follows. + + + Match against the list of modules in the input ```rust FfiItems```, + searching for the leftmost extracted segment of the input import's path. + + - If the list of modules contains a match against the path, proceed as + follows. + + + Run algorithm 2. Set the input ```rust use``` statement to be the + rhs of the current import statement. Set the input + ```rust FfiItems``` to be the match module. + + + Return the result of step 1.a.1.a.1. + + - If the list of modules does not contain a match, proceed as follows. + + + Return a singleton list. Its one element should consist of the value + returned from calling the _unresolved_ data constructor. + + - If the import is an identifier or a renamed identifier, proceed as + follows. + + + Match against the input ```rust FfiItems```'s list of items. + + - If a match is found for the identifier or original identifier (in the + case of a rename,) proceed as follows. + + + Return a singleton list. Its one element should consist of a new + ```rust FfiItems``` instance containing solely the found item, + wrapped by a _resolved_ data constructor. + + - If no match is found, proceed as follows. + + + Return a singleton list. The element should consist of the value + returned from calling the _unresolved_ data constructor. + + - If the import is a glob, proceed as follows. + + + Return a single-element list. Its one element should wrap the input + ```rust FfiItems``` instance with a _resolved_ data constructor. + + - If the import is a group, proceed as follows. + + + Run algorithm 3. Set the input list to be the matched group. Set the + input ```rust FfiItems``` to be the current input's ```rust FfiItems```. + + + Return the result of step 1.d.1. + +/ Algorithm 3: \ + Inputs: + + - A list of grouped elements in the tail of an import statement. + + - An ```rust FfiItems``` instance where the reexport from which the above + input is sourced (i.e. the instance containing the whole ```rust use``` + statement.) + + Outputs: + + - A list of ```rust FfiItems``` wrapped with the same sum type as outlined in + the outputs of algorithm 2. + + Steps: + + + Match against the input list. + + - If there are no elements left in the group, return the empty list. + + - If there are any elements left, extract the next element and proceed as + follows. + + + Run algorithm 2. Set the input import statement to be the extracted + element. Set the input ```rust FfiItems``` to be the current input + ```rust FfiItems```. + + + Run algorithm 3. Set te input list to be the tail list of elements after + extracting the above element. Set the ```rust FfiItems``` instance to be + the same input instance as we currently have as input. + + + Return the resulting list from appending the lists from step 1.b.1 to + the lists from step 1.b.2. + +/ Algorithm 4: \ + Inputs: + + - An instance of ```rust FfiItems```. + + Outputs: + + - A list of all reexports in the input ```rust FfiItems``` mapped to the type + outlined in the outputs of algorithm 2. + + Steps: + + + Match against the list of reexports in the input ```rust FfiItems```. + + - If there are no reexports, return the empty list. + + - If there are any reexports left, proceed as follows. + + + Run algorithm 2. Set the input import to be the matched reexport. Set + the input ```rust FfiItems``` to be the current input's + ```rust FfiItems```. + + + Run algorithm 4. Set the input ```rust FfiItems``` to be a new + ```rust FfiItems``` instance whose modules contain the current input's + tail list of modules, barring the extracted reexport. + + + Return the result of appending the resulting list from step 1.b.1 to the + resulting list from step 1.b.2.